@alternative-path/testlens-playwright-reporter 0.4.9 → 0.4.11
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 +248 -246
- package/cross-env.js +2 -2
- package/index.d.ts +17 -1
- package/index.js +275 -199
- package/index.ts +320 -201
- package/package.json +75 -75
package/index.ts
CHANGED
|
@@ -8,69 +8,154 @@ import type { Reporter, TestCase, TestResult, FullConfig, Suite } from '@playwri
|
|
|
8
8
|
import { execSync } from 'child_process';
|
|
9
9
|
import pino from 'pino';
|
|
10
10
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
'
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
const shouldSuppressInfo = (msg: string): boolean => {
|
|
40
|
-
if (isVerboseLogs) return false;
|
|
41
|
-
if (!msg) return false;
|
|
42
|
-
return (
|
|
43
|
-
noisyInfoPrefixes.some(prefix => msg.startsWith(prefix)) ||
|
|
44
|
-
noisyInfoSubstrings.some(sub => msg.includes(sub))
|
|
45
|
-
);
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
// Create pino logger instance
|
|
49
|
-
const logger = pino({
|
|
50
|
-
level: process.env.TESTLENS_LOG_LEVEL || process.env.LOG_LEVEL || 'info',
|
|
51
|
-
base: null, // remove pid/hostname
|
|
52
|
-
timestamp: false, // remove time
|
|
53
|
-
formatters: {
|
|
54
|
-
level: () => ({}) // remove level
|
|
55
|
-
},
|
|
56
|
-
hooks: {
|
|
57
|
-
logMethod(args, method, level) {
|
|
58
|
-
// pino level is numeric (info = 30)
|
|
59
|
-
if (level === 30) {
|
|
60
|
-
const msg =
|
|
61
|
-
typeof args[0] === 'string'
|
|
62
|
-
? args[0]
|
|
63
|
-
: typeof args[1] === 'string'
|
|
64
|
-
? args[1]
|
|
65
|
-
: '';
|
|
66
|
-
if (shouldSuppressInfo(msg)) {
|
|
67
|
-
return;
|
|
11
|
+
const TESTLENS_X_WORKFLOW_AUTH =
|
|
12
|
+
'd5f8f1d1a4546e9b49dcedbe51f483aca4bb0e2357b513082b0ce49e59583d38';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Logger class for TestLens Reporter with 2-level logging support
|
|
16
|
+
* - 'info' level: Shows test start/completion with status, artifact status, and errors
|
|
17
|
+
* - 'debug' level: Shows all logs including detailed internal operations
|
|
18
|
+
*
|
|
19
|
+
* CloudWatch-compatible: outputs JSON with level field for filtering
|
|
20
|
+
*/
|
|
21
|
+
class Logger {
|
|
22
|
+
private pinoLogger: pino.Logger;
|
|
23
|
+
private logLevel: 'info' | 'debug';
|
|
24
|
+
private runId?: string;
|
|
25
|
+
|
|
26
|
+
constructor(logLevel: 'info' | 'debug' = 'info') {
|
|
27
|
+
this.logLevel = logLevel;
|
|
28
|
+
|
|
29
|
+
// Create base pino logger with CloudWatch-compatible settings
|
|
30
|
+
this.pinoLogger = pino({
|
|
31
|
+
level: 'debug', // Always log at debug level, we filter manually
|
|
32
|
+
base: null, // remove pid/hostname - CloudWatch provides this
|
|
33
|
+
timestamp: false, // CloudWatch adds timestamps
|
|
34
|
+
formatters: {
|
|
35
|
+
level: (label) => {
|
|
36
|
+
return { level: label }; // Include level for CloudWatch filtering
|
|
68
37
|
}
|
|
69
38
|
}
|
|
70
|
-
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Set run ID context for all subsequent logs
|
|
44
|
+
* This allows correlating all logs to a specific test run in CloudWatch
|
|
45
|
+
*/
|
|
46
|
+
setRunId(runId: string): void {
|
|
47
|
+
this.runId = runId;
|
|
48
|
+
|
|
49
|
+
// Recreate logger with runId context for CloudWatch querying
|
|
50
|
+
this.pinoLogger = this.pinoLogger.child({ runId });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if we're in debug mode
|
|
55
|
+
*/
|
|
56
|
+
isDebug(): boolean {
|
|
57
|
+
return this.logLevel === 'debug';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Debug level logging - only shown when logLevel is 'debug'
|
|
62
|
+
*/
|
|
63
|
+
debug(msg: string, ...args: any[]): void {
|
|
64
|
+
if (this.logLevel === 'debug') {
|
|
65
|
+
this.pinoLogger.debug(msg, ...args);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Debug level logging with object
|
|
71
|
+
*/
|
|
72
|
+
debugObj(obj: Record<string, any>, msg?: string): void {
|
|
73
|
+
if (this.logLevel === 'debug') {
|
|
74
|
+
this.pinoLogger.debug(obj, msg);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Info level logging - shown for both 'info' and 'debug' levels
|
|
80
|
+
*/
|
|
81
|
+
info(msg: string, ...args: any[]): void {
|
|
82
|
+
this.pinoLogger.info(msg, ...args);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Info level logging with object
|
|
87
|
+
*/
|
|
88
|
+
infoObj(obj: Record<string, any>, msg?: string): void {
|
|
89
|
+
this.pinoLogger.info(obj, msg);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Warn level logging - shown for both 'info' and 'debug' levels
|
|
94
|
+
*/
|
|
95
|
+
warn(msg: string, ...args: any[]): void {
|
|
96
|
+
this.pinoLogger.warn(msg, ...args);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Error level logging - always shown regardless of log level
|
|
101
|
+
*/
|
|
102
|
+
error(msg: string | Record<string, any>, ...args: any[]): void {
|
|
103
|
+
if (typeof msg === 'string') {
|
|
104
|
+
this.pinoLogger.error(msg, ...args);
|
|
105
|
+
} else {
|
|
106
|
+
this.pinoLogger.error(msg, ...args);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Log test case start - debug level only
|
|
112
|
+
* In info mode, we only show logs when there are issues
|
|
113
|
+
*/
|
|
114
|
+
logTestStart(testName: string): void {
|
|
115
|
+
this.debug(`[TEST START] ${testName}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Log test case completion - debug level only
|
|
120
|
+
* In info mode, we only show logs when there are issues
|
|
121
|
+
*/
|
|
122
|
+
logTestEnd(testName: string, status: string, duration: number): void {
|
|
123
|
+
const statusEmoji = status === 'passed' ? '✓' : status === 'failed' ? '✗' : '○';
|
|
124
|
+
const durationStr = `${duration}ms`;
|
|
125
|
+
this.debug(`[TEST END] ${statusEmoji} ${testName} - ${status} (${durationStr})`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Log artifact status - info level only for failures
|
|
130
|
+
* Success is logged at debug level
|
|
131
|
+
*/
|
|
132
|
+
logArtifactStatus(testName: string, artifactType: string, status: 'uploaded' | 'skipped' | 'failed'): void {
|
|
133
|
+
if (status === 'failed') {
|
|
134
|
+
this.error(`[ARTIFACT] ✗ ${artifactType} for "${testName}" - failed`);
|
|
135
|
+
} else {
|
|
136
|
+
this.debug(`[ARTIFACT] ${status === 'uploaded' ? '✓' : '○'} ${artifactType} for "${testName}" - ${status}`);
|
|
71
137
|
}
|
|
72
138
|
}
|
|
73
|
-
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Log run summary - debug level only
|
|
142
|
+
* In info mode, we only show logs when there are issues
|
|
143
|
+
*/
|
|
144
|
+
logRunSummary(passed: number, failed: number, skipped: number, timedOut: number): void {
|
|
145
|
+
this.debug(`[RUN SUMMARY] ${passed} passed, ${failed} failed (${timedOut} timeouts), ${skipped} skipped`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Legacy logger for backward compatibility during transition
|
|
150
|
+
// Will be replaced with Logger instance per reporter
|
|
151
|
+
let globalLogger = new Logger('info');
|
|
152
|
+
const logger = {
|
|
153
|
+
get debug() { return globalLogger.debug.bind(globalLogger); },
|
|
154
|
+
get info() { return globalLogger.info.bind(globalLogger); },
|
|
155
|
+
get warn() { return globalLogger.warn.bind(globalLogger); },
|
|
156
|
+
get error() { return globalLogger.error.bind(globalLogger); },
|
|
157
|
+
get levelVal() { return globalLogger.isDebug() ? 20 : 30; }
|
|
158
|
+
};
|
|
74
159
|
|
|
75
160
|
// Lazy-load mime module to support ESM
|
|
76
161
|
let mimeModule: any = null;
|
|
@@ -114,6 +199,12 @@ export interface TestLensReporterConfig {
|
|
|
114
199
|
customMetadata?: Record<string, string | string[]>;
|
|
115
200
|
/** Execution ID for one run per pipeline (e.g. from TESTLENS_EXECUTION_ID or CI build UUID). When set, used as runId so multiple steps share one run. */
|
|
116
201
|
executionId?: string;
|
|
202
|
+
/**
|
|
203
|
+
* Log level for the reporter.
|
|
204
|
+
* - 'info' (default): Shows test start/completion with status, artifact status, and errors
|
|
205
|
+
* - 'debug': Shows all logs including detailed internal operations
|
|
206
|
+
*/
|
|
207
|
+
logLevel?: 'info' | 'debug';
|
|
117
208
|
}
|
|
118
209
|
|
|
119
210
|
export interface TestLensReporterOptions {
|
|
@@ -147,6 +238,12 @@ export interface TestLensReporterOptions {
|
|
|
147
238
|
customMetadata?: Record<string, string | string[]>;
|
|
148
239
|
/** Execution ID for one run per pipeline (e.g. from TESTLENS_EXECUTION_ID or CI build UUID). When set, used as runId so multiple steps share one run. */
|
|
149
240
|
executionId?: string;
|
|
241
|
+
/**
|
|
242
|
+
* Log level for the reporter.
|
|
243
|
+
* - 'info' (default): Shows test start/completion with status, artifact status, and errors
|
|
244
|
+
* - 'debug': Shows all logs including detailed internal operations
|
|
245
|
+
*/
|
|
246
|
+
logLevel?: 'info' | 'debug';
|
|
150
247
|
}
|
|
151
248
|
|
|
152
249
|
export interface GitInfo {
|
|
@@ -256,13 +353,24 @@ export class TestLensReporter implements Reporter {
|
|
|
256
353
|
private runCreationFailed: boolean = false; // Track if run creation failed due to limits
|
|
257
354
|
private cliArgs: Record<string, any> = {}; // Store CLI args separately
|
|
258
355
|
private pendingUploads: Set<Promise<any>> = new Set(); // Track pending artifact uploads
|
|
259
|
-
private traceNetworkRows: unknown[] = []; // Network requests/responses from trace zip for current test
|
|
260
356
|
private artifactStats = {
|
|
261
357
|
uploaded: { screenshot: 0, video: 0, trace: 0, attachment: 0 },
|
|
262
358
|
skipped: { screenshot: 0, video: 0, trace: 0, attachment: 0 },
|
|
263
359
|
failed: { screenshot: 0, video: 0, trace: 0, attachment: 0 }
|
|
264
360
|
};
|
|
265
361
|
private artifactsSeen: number = 0;
|
|
362
|
+
private logger: Logger; // Instance logger with proper context
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Get environment variable value with case-insensitive key match (e.g. TL_BUILDNAME, tl_buildname).
|
|
366
|
+
*/
|
|
367
|
+
private static getEnvCaseInsensitive(name: string): string | undefined {
|
|
368
|
+
const upper = name.toUpperCase();
|
|
369
|
+
for (const key of Object.keys(process.env)) {
|
|
370
|
+
if (key.toUpperCase() === upper) return process.env[key];
|
|
371
|
+
}
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
266
374
|
|
|
267
375
|
/**
|
|
268
376
|
* Parse custom metadata from environment variables
|
|
@@ -273,11 +381,12 @@ export class TestLensReporter implements Reporter {
|
|
|
273
381
|
|
|
274
382
|
// Common environment variable names for build metadata
|
|
275
383
|
const envVarMappings: Record<string, string[]> = {
|
|
276
|
-
// Support both TestLens-specific names (recommended) and common CI names
|
|
277
|
-
'testlensBuildTag': ['testlensBuildTag', 'TESTLENS_BUILD_TAG', 'TESTLENS_BUILDTAG', 'BUILDTAG', 'BUILD_TAG', 'TestlensBuildTag', 'TestLensBuildTag'],
|
|
278
|
-
'testlensBuildName': ['testlensBuildName', 'TESTLENS_BUILD_NAME', 'TESTLENS_BUILDNAME', 'BUILDNAME', 'BUILD_NAME', 'TestlensBuildName', 'TestLensBuildName'],
|
|
384
|
+
// Support both TestLens-specific names (recommended) and common CI names; TL_* are short aliases; keys matched case-insensitively
|
|
385
|
+
'testlensBuildTag': ['TL_BUILDTAG', 'testlensBuildTag', 'TESTLENS_BUILD_TAG', 'TESTLENS_BUILDTAG', 'BUILDTAG', 'BUILD_TAG', 'TestlensBuildTag', 'TestLensBuildTag'],
|
|
386
|
+
'testlensBuildName': ['TL_BUILDNAME', 'testlensBuildName', 'TESTLENS_BUILD_NAME', 'TESTLENS_BUILDNAME', 'BUILDNAME', 'BUILD_NAME', 'TestlensBuildName', 'TestLensBuildName'],
|
|
279
387
|
// Execution ID for one run per pipeline (checked in order; prefer TESTLENS_EXECUTION_ID, then CI-specific UUIDs)
|
|
280
388
|
'executionId': [
|
|
389
|
+
'TL_EXECUTIONID',
|
|
281
390
|
'TESTLENS_EXECUTION_ID',
|
|
282
391
|
'TestlensExecutionId',
|
|
283
392
|
'TestLensExecutionId',
|
|
@@ -300,10 +409,10 @@ export class TestLensReporter implements Reporter {
|
|
|
300
409
|
'customvalue': ['CUSTOMVALUE', 'CUSTOM_VALUE']
|
|
301
410
|
};
|
|
302
411
|
|
|
303
|
-
// Check for each metadata key
|
|
412
|
+
// Check for each metadata key (case-insensitive env lookup)
|
|
304
413
|
Object.entries(envVarMappings).forEach(([key, envVars]) => {
|
|
305
414
|
for (const envVar of envVars) {
|
|
306
|
-
const value =
|
|
415
|
+
const value = TestLensReporter.getEnvCaseInsensitive(envVar);
|
|
307
416
|
if (value) {
|
|
308
417
|
// For testlensBuildTag, support comma-separated values
|
|
309
418
|
if (key === 'testlensBuildTag' && value.includes(',')) {
|
|
@@ -322,6 +431,16 @@ export class TestLensReporter implements Reporter {
|
|
|
322
431
|
}
|
|
323
432
|
|
|
324
433
|
constructor(options: TestLensReporterOptions) {
|
|
434
|
+
// Initialize logger first with user-configured log level
|
|
435
|
+
const logLevel = options.logLevel ||
|
|
436
|
+
(process.env.TESTLENS_LOG_LEVEL as 'info' | 'debug') ||
|
|
437
|
+
(process.env.LOG_LEVEL === 'debug' ? 'debug' : 'info') ||
|
|
438
|
+
'info';
|
|
439
|
+
this.logger = new Logger(logLevel);
|
|
440
|
+
|
|
441
|
+
// Update global logger reference for any legacy code
|
|
442
|
+
globalLogger = this.logger;
|
|
443
|
+
|
|
325
444
|
// Parse custom CLI arguments
|
|
326
445
|
const customArgs = TestLensReporter.parseCustomArgs();
|
|
327
446
|
this.cliArgs = customArgs; // Store CLI args separately for later use
|
|
@@ -354,7 +473,8 @@ export class TestLensReporter implements Reporter {
|
|
|
354
473
|
rejectUnauthorized: options.rejectUnauthorized,
|
|
355
474
|
ignoreSslErrors: options.ignoreSslErrors,
|
|
356
475
|
customMetadata: { ...options.customMetadata, ...customArgs }, // Config metadata first, then CLI args override
|
|
357
|
-
executionId: options.executionId
|
|
476
|
+
executionId: options.executionId,
|
|
477
|
+
logLevel: logLevel
|
|
358
478
|
} as Required<TestLensReporterConfig>;
|
|
359
479
|
|
|
360
480
|
if (!this.config.apiKey) {
|
|
@@ -362,7 +482,7 @@ export class TestLensReporter implements Reporter {
|
|
|
362
482
|
}
|
|
363
483
|
|
|
364
484
|
if (apiKey !== options.apiKey) {
|
|
365
|
-
logger.debug('✓ Using API key from environment variable');
|
|
485
|
+
this.logger.debug('✓ Using API key from environment variable');
|
|
366
486
|
}
|
|
367
487
|
|
|
368
488
|
// Default environment to allow self-signed certs unless explicitly set
|
|
@@ -376,15 +496,15 @@ export class TestLensReporter implements Reporter {
|
|
|
376
496
|
// Check various ways SSL validation can be disabled or enforced (in order of precedence)
|
|
377
497
|
if (this.config.ignoreSslErrors === true) {
|
|
378
498
|
rejectUnauthorized = false;
|
|
379
|
-
logger.debug('[DEBUG] SSL certificate validation disabled via ignoreSslErrors option');
|
|
499
|
+
this.logger.debug('[DEBUG] SSL certificate validation disabled via ignoreSslErrors option');
|
|
380
500
|
} else if (this.config.rejectUnauthorized === false) {
|
|
381
501
|
rejectUnauthorized = false;
|
|
382
|
-
logger.debug('[DEBUG] SSL certificate validation disabled via rejectUnauthorized option');
|
|
502
|
+
this.logger.debug('[DEBUG] SSL certificate validation disabled via rejectUnauthorized option');
|
|
383
503
|
} else if (this.config.rejectUnauthorized === true) {
|
|
384
504
|
rejectUnauthorized = true;
|
|
385
505
|
} else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') {
|
|
386
506
|
rejectUnauthorized = false;
|
|
387
|
-
logger.debug('[DEBUG] SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
|
|
507
|
+
this.logger.debug('[DEBUG] SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
|
|
388
508
|
} else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '1') {
|
|
389
509
|
rejectUnauthorized = true;
|
|
390
510
|
}
|
|
@@ -398,6 +518,7 @@ export class TestLensReporter implements Reporter {
|
|
|
398
518
|
timeout: this.config.timeout,
|
|
399
519
|
headers: {
|
|
400
520
|
'Content-Type': 'application/json',
|
|
521
|
+
'x-workflow-auth': TESTLENS_X_WORKFLOW_AUTH,
|
|
401
522
|
...(this.config.apiKey && { 'X-API-Key': this.config.apiKey }),
|
|
402
523
|
},
|
|
403
524
|
// Enhanced SSL handling with flexible TLS configuration
|
|
@@ -444,6 +565,10 @@ export class TestLensReporter implements Reporter {
|
|
|
444
565
|
const executionId = typeof executionIdRaw === 'string' && executionIdRaw.trim() ? String(executionIdRaw).trim() : undefined;
|
|
445
566
|
this.runId = executionId || randomUUID();
|
|
446
567
|
this.usedExecutionId = !!executionId;
|
|
568
|
+
|
|
569
|
+
// Set runId in logger context for CloudWatch correlation
|
|
570
|
+
this.logger.setRunId(this.runId);
|
|
571
|
+
|
|
447
572
|
this.runMetadata = this.initializeRunMetadata();
|
|
448
573
|
this.specMap = new Map<string, SpecData>();
|
|
449
574
|
this.testMap = new Map<string, TestData>();
|
|
@@ -451,11 +576,11 @@ export class TestLensReporter implements Reporter {
|
|
|
451
576
|
|
|
452
577
|
// Log custom metadata if any
|
|
453
578
|
if (this.config.customMetadata && Object.keys(this.config.customMetadata).length > 0) {
|
|
454
|
-
logger.debug('\n[METADATA] Custom Metadata Detected:');
|
|
579
|
+
this.logger.debug('\n[METADATA] Custom Metadata Detected:');
|
|
455
580
|
Object.entries(this.config.customMetadata).forEach(([key, value]) => {
|
|
456
|
-
logger.debug(` ${key}: ${value}`);
|
|
581
|
+
this.logger.debug(` ${key}: ${value}`);
|
|
457
582
|
});
|
|
458
|
-
logger.debug('');
|
|
583
|
+
this.logger.debug('');
|
|
459
584
|
}
|
|
460
585
|
}
|
|
461
586
|
|
|
@@ -525,25 +650,20 @@ export class TestLensReporter implements Reporter {
|
|
|
525
650
|
}
|
|
526
651
|
|
|
527
652
|
async onBegin(config: FullConfig, suite: Suite): Promise<void> {
|
|
528
|
-
logger.debug(`[REPORTER] source=${__filename} enableArtifacts=${this.config.enableArtifacts}`);
|
|
529
|
-
//
|
|
530
|
-
|
|
531
|
-
logger.debug(`TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
|
|
532
|
-
logger.debug(` Run ID: ${this.runId}`);
|
|
533
|
-
} else {
|
|
534
|
-
logger.debug(`TestLens Reporter starting - Run ID: ${this.runId}`);
|
|
535
|
-
}
|
|
653
|
+
this.logger.debug(`[REPORTER] source=${__filename} enableArtifacts=${this.config.enableArtifacts}`);
|
|
654
|
+
// Only show startup info in debug mode - in info mode we only log issues
|
|
655
|
+
this.logger.debug(`TestLens Reporter starting - Run ID: ${this.runId}`);
|
|
536
656
|
|
|
537
657
|
// Collect Git information if enabled
|
|
538
658
|
if (this.config.enableGitInfo) {
|
|
539
659
|
this.runMetadata.gitInfo = await this.collectGitInfo();
|
|
540
660
|
if (this.runMetadata.gitInfo) {
|
|
541
|
-
logger.debug(`Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
|
|
661
|
+
this.logger.debug(`Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
|
|
542
662
|
} else {
|
|
543
|
-
logger.warn(`[WARN] Git info collection returned null - not in a git repository or git not available`);
|
|
663
|
+
this.logger.warn(`[WARN] Git info collection returned null - not in a git repository or git not available`);
|
|
544
664
|
}
|
|
545
665
|
} else {
|
|
546
|
-
logger.debug(`[INFO] Git info collection disabled (enableGitInfo: false)`);
|
|
666
|
+
this.logger.debug(`[INFO] Git info collection disabled (enableGitInfo: false)`);
|
|
547
667
|
}
|
|
548
668
|
|
|
549
669
|
// Add shard information if available
|
|
@@ -564,8 +684,8 @@ export class TestLensReporter implements Reporter {
|
|
|
564
684
|
}
|
|
565
685
|
|
|
566
686
|
async onTestBegin(test: TestCase, result: TestResult): Promise<void> {
|
|
567
|
-
// Log which test is starting
|
|
568
|
-
logger.
|
|
687
|
+
// Log which test is starting (info level for test start)
|
|
688
|
+
this.logger.logTestStart(test.title);
|
|
569
689
|
|
|
570
690
|
const specPath = test.location.file;
|
|
571
691
|
const specKey = `${specPath}-${test.parent.title}`;
|
|
@@ -643,30 +763,16 @@ export class TestLensReporter implements Reporter {
|
|
|
643
763
|
}
|
|
644
764
|
|
|
645
765
|
async onTestEnd(test: TestCase, result: TestResult): Promise<void> {
|
|
646
|
-
this.
|
|
766
|
+
this.logger.debug(`[ARTIFACTS] attachments=${result.attachments?.length ?? 0}`);
|
|
767
|
+
|
|
647
768
|
const testId = this.getTestId(test);
|
|
648
769
|
let testCaseId = '';
|
|
770
|
+
const localTraceNetworkRows: unknown[] = [];
|
|
649
771
|
let testData = this.testMap.get(testId);
|
|
650
772
|
|
|
651
|
-
logger.debug(`[ARTIFACTS] attachments=${result.attachments?.length ?? 0}`);
|
|
652
|
-
|
|
653
|
-
if (result.attachments && result.attachments.length > 0) {
|
|
654
|
-
for (const attachment of result.attachments) {
|
|
655
|
-
const artifactType = this.getArtifactType(attachment.name);
|
|
656
|
-
this.artifactsSeen += 1;
|
|
657
|
-
if (attachment.path) {
|
|
658
|
-
this.bumpArtifactStat('uploaded', artifactType);
|
|
659
|
-
} else {
|
|
660
|
-
this.bumpArtifactStat('skipped', artifactType);
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
logger.debug(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
|
|
666
|
-
|
|
667
773
|
// For skipped tests, onTestBegin might not be called, so we need to create the test data here
|
|
668
774
|
if (!testData) {
|
|
669
|
-
logger.debug(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
|
|
775
|
+
this.logger.debug(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
|
|
670
776
|
// Create spec data if not exists (skipped tests might not have spec data either)
|
|
671
777
|
const specPath = test.location.file;
|
|
672
778
|
const specKey = `${specPath}-${test.parent.title}`;
|
|
@@ -832,7 +938,7 @@ export class TestLensReporter implements Reporter {
|
|
|
832
938
|
|
|
833
939
|
// Send testEnd event for all tests, regardless of status
|
|
834
940
|
// This ensures tests that are interrupted or have unexpected statuses are properly recorded
|
|
835
|
-
logger.debug(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
|
|
941
|
+
this.logger.debug(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
|
|
836
942
|
// Send test end event to API and get response
|
|
837
943
|
const testEndResponse = await this.sendToApi({
|
|
838
944
|
type: 'testEnd',
|
|
@@ -842,14 +948,17 @@ export class TestLensReporter implements Reporter {
|
|
|
842
948
|
});
|
|
843
949
|
testCaseId = testEndResponse?.testCaseId;
|
|
844
950
|
|
|
951
|
+
// Log test completion with status (info level)
|
|
952
|
+
this.logger.logTestEnd(test.title, testData.status, testData.duration);
|
|
953
|
+
|
|
845
954
|
// Handle artifacts (test case is now guaranteed to be in database)
|
|
846
955
|
if (this.config.enableArtifacts) {
|
|
847
956
|
// Pass test case DB ID if available for faster lookups; pass status/endTime so backend
|
|
848
957
|
// can fix up test case status if testEnd failed but artifact request succeeds
|
|
849
|
-
await this.processArtifacts(testId, result, testEndResponse?.testCaseId, {
|
|
958
|
+
await this.processArtifacts(testId, test.title, result, testEndResponse?.testCaseId, {
|
|
850
959
|
status: testData.status,
|
|
851
960
|
endTime: testData.endTime
|
|
852
|
-
});
|
|
961
|
+
}, localTraceNetworkRows);
|
|
853
962
|
} else if (result.attachments && result.attachments.length > 0) {
|
|
854
963
|
for (const attachment of result.attachments) {
|
|
855
964
|
const artifactType = this.getArtifactType(attachment.name);
|
|
@@ -922,7 +1031,7 @@ export class TestLensReporter implements Reporter {
|
|
|
922
1031
|
});
|
|
923
1032
|
|
|
924
1033
|
// Send spec code blocks to API
|
|
925
|
-
await this.sendSpecCodeBlocks(specPath, test.title, testData?.errors || [], this.runId, testId, testCaseId);
|
|
1034
|
+
await this.sendSpecCodeBlocks(specPath, test.title, testData?.errors || [], this.runId, testId, testCaseId, localTraceNetworkRows);
|
|
926
1035
|
}
|
|
927
1036
|
}
|
|
928
1037
|
}
|
|
@@ -933,29 +1042,36 @@ export class TestLensReporter implements Reporter {
|
|
|
933
1042
|
|
|
934
1043
|
// Wait for all pending artifact uploads to complete before sending runEnd
|
|
935
1044
|
if (this.pendingUploads.size > 0) {
|
|
936
|
-
logger.debug(`Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
|
|
1045
|
+
this.logger.debug(`Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
|
|
937
1046
|
try {
|
|
938
1047
|
await Promise.all(Array.from(this.pendingUploads));
|
|
939
|
-
logger.debug(`[OK] All artifact uploads completed`);
|
|
1048
|
+
this.logger.debug(`[OK] All artifact uploads completed`);
|
|
940
1049
|
} catch (error) {
|
|
941
|
-
logger.error(`[WARN] Some artifact uploads failed, continuing with runEnd`);
|
|
1050
|
+
this.logger.error(`[WARN] Some artifact uploads failed, continuing with runEnd`);
|
|
942
1051
|
}
|
|
943
1052
|
}
|
|
944
1053
|
|
|
945
1054
|
const uploaded = this.artifactStats.uploaded;
|
|
946
1055
|
const skipped = this.artifactStats.skipped;
|
|
947
1056
|
const failed = this.artifactStats.failed;
|
|
948
|
-
|
|
1057
|
+
|
|
1058
|
+
// Calculate total uploaded/failed for info level summary
|
|
1059
|
+
const totalUploaded = uploaded.screenshot + uploaded.video + uploaded.trace + uploaded.attachment;
|
|
1060
|
+
const totalFailed = failed.screenshot + failed.video + failed.trace + failed.attachment;
|
|
1061
|
+
const totalSkipped = skipped.screenshot + skipped.video + skipped.trace + skipped.attachment;
|
|
1062
|
+
|
|
1063
|
+
// Info level: only show artifact summary if there are failures
|
|
1064
|
+
if (totalFailed > 0) {
|
|
1065
|
+
this.logger.error(`[ARTIFACTS] ${totalUploaded} uploaded, ${totalFailed} failed, ${totalSkipped} skipped`);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// Debug level: show detailed breakdown
|
|
949
1069
|
const summary =
|
|
950
1070
|
`[ARTIFACTS] seen=${this.artifactsSeen} | ` +
|
|
951
1071
|
`uploaded screenshot=${uploaded.screenshot}, video=${uploaded.video}, trace=${uploaded.trace}, attachment=${uploaded.attachment} | ` +
|
|
952
1072
|
`skipped screenshot=${skipped.screenshot}, video=${skipped.video}, trace=${skipped.trace}, attachment=${skipped.attachment} | ` +
|
|
953
1073
|
`failed screenshot=${failed.screenshot}, video=${failed.video}, trace=${failed.trace}, attachment=${failed.attachment}`;
|
|
954
|
-
|
|
955
|
-
console.log(summary);
|
|
956
|
-
} else {
|
|
957
|
-
logger.debug(summary);
|
|
958
|
-
}
|
|
1074
|
+
this.logger.debug(summary);
|
|
959
1075
|
|
|
960
1076
|
// Calculate final stats
|
|
961
1077
|
const totalTests = Array.from(this.testMap.values()).length;
|
|
@@ -987,14 +1103,11 @@ export class TestLensReporter implements Reporter {
|
|
|
987
1103
|
}
|
|
988
1104
|
});
|
|
989
1105
|
|
|
990
|
-
//
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
logger.debug(`[COMPLETE] TestLens Report completed - Run ID: ${this.runId}`);
|
|
996
|
-
}
|
|
997
|
-
logger.debug(`[RESULTS] ${passedTests} passed, ${failedTests} failed (${timedOutTests} timeouts), ${skippedTests} skipped`);
|
|
1106
|
+
// Only show completion info in debug mode - in info mode we only log issues
|
|
1107
|
+
this.logger.debug(`[COMPLETE] TestLens Report completed - Run ID: ${this.runId}`);
|
|
1108
|
+
|
|
1109
|
+
// Log run summary at debug level
|
|
1110
|
+
this.logger.logRunSummary(passedTests, failedTests, skippedTests, timedOutTests);
|
|
998
1111
|
}
|
|
999
1112
|
|
|
1000
1113
|
private async sendToApi(payload: any): Promise<any> {
|
|
@@ -1010,8 +1123,9 @@ export class TestLensReporter implements Reporter {
|
|
|
1010
1123
|
}
|
|
1011
1124
|
});
|
|
1012
1125
|
if (this.config.enableRealTimeStream) {
|
|
1013
|
-
logger.debug(`[OK] Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
|
|
1126
|
+
this.logger.debug(`[OK] Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
|
|
1014
1127
|
}
|
|
1128
|
+
|
|
1015
1129
|
// Return response data for caller to use
|
|
1016
1130
|
return response.data;
|
|
1017
1131
|
} catch (error: any) {
|
|
@@ -1025,25 +1139,25 @@ export class TestLensReporter implements Reporter {
|
|
|
1025
1139
|
this.runCreationFailed = true;
|
|
1026
1140
|
}
|
|
1027
1141
|
|
|
1028
|
-
logger.error('\n' + '='.repeat(80));
|
|
1142
|
+
this.logger.error('\n' + '='.repeat(80));
|
|
1029
1143
|
if (errorData?.limit_type === 'test_cases') {
|
|
1030
|
-
logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
|
|
1144
|
+
this.logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
|
|
1031
1145
|
} else if (errorData?.limit_type === 'test_runs') {
|
|
1032
|
-
logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
|
|
1146
|
+
this.logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
|
|
1033
1147
|
} else {
|
|
1034
|
-
logger.error('[ERROR] TESTLENS ERROR: Plan Limit Reached');
|
|
1148
|
+
this.logger.error('[ERROR] TESTLENS ERROR: Plan Limit Reached');
|
|
1035
1149
|
}
|
|
1036
|
-
logger.error('='.repeat(80));
|
|
1037
|
-
logger.error('');
|
|
1038
|
-
logger.error(errorData?.message || 'You have reached your plan limit.');
|
|
1039
|
-
logger.error('');
|
|
1040
|
-
logger.error(`Current usage: ${errorData?.current_usage || 'N/A'} / ${errorData?.limit || 'N/A'}`);
|
|
1041
|
-
logger.error('');
|
|
1042
|
-
logger.error('To continue, please upgrade your plan.');
|
|
1043
|
-
logger.error('Contact: support@alternative-path.com');
|
|
1044
|
-
logger.error('');
|
|
1045
|
-
logger.error('='.repeat(80));
|
|
1046
|
-
logger.error('');
|
|
1150
|
+
this.logger.error('='.repeat(80));
|
|
1151
|
+
this.logger.error('');
|
|
1152
|
+
this.logger.error(errorData?.message || 'You have reached your plan limit.');
|
|
1153
|
+
this.logger.error('');
|
|
1154
|
+
this.logger.error(`Current usage: ${errorData?.current_usage || 'N/A'} / ${errorData?.limit || 'N/A'}`);
|
|
1155
|
+
this.logger.error('');
|
|
1156
|
+
this.logger.error('To continue, please upgrade your plan.');
|
|
1157
|
+
this.logger.error('Contact: support@alternative-path.com');
|
|
1158
|
+
this.logger.error('');
|
|
1159
|
+
this.logger.error('='.repeat(80));
|
|
1160
|
+
this.logger.error('');
|
|
1047
1161
|
return; // Don't log the full error object for limit errors
|
|
1048
1162
|
}
|
|
1049
1163
|
|
|
@@ -1051,35 +1165,35 @@ export class TestLensReporter implements Reporter {
|
|
|
1051
1165
|
if (status === 401) {
|
|
1052
1166
|
if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
|
|
1053
1167
|
errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
|
|
1054
|
-
logger.error('\n' + '='.repeat(80));
|
|
1168
|
+
this.logger.error('\n' + '='.repeat(80));
|
|
1055
1169
|
|
|
1056
1170
|
if (errorData?.error === 'test_cases_limit_reached') {
|
|
1057
|
-
logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
|
|
1171
|
+
this.logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
|
|
1058
1172
|
} else if (errorData?.error === 'test_runs_limit_reached') {
|
|
1059
|
-
logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
|
|
1173
|
+
this.logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
|
|
1060
1174
|
} else {
|
|
1061
|
-
logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
|
|
1175
|
+
this.logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
|
|
1062
1176
|
}
|
|
1063
1177
|
|
|
1064
|
-
logger.error('='.repeat(80));
|
|
1065
|
-
logger.error('');
|
|
1066
|
-
logger.error(errorData?.message || 'Your trial period has expired.');
|
|
1067
|
-
logger.error('');
|
|
1068
|
-
logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
|
|
1069
|
-
logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
|
|
1070
|
-
logger.error('');
|
|
1178
|
+
this.logger.error('='.repeat(80));
|
|
1179
|
+
this.logger.error('');
|
|
1180
|
+
this.logger.error(errorData?.message || 'Your trial period has expired.');
|
|
1181
|
+
this.logger.error('');
|
|
1182
|
+
this.logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
|
|
1183
|
+
this.logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
|
|
1184
|
+
this.logger.error('');
|
|
1071
1185
|
if (errorData?.trial_end_date) {
|
|
1072
|
-
logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
|
|
1073
|
-
logger.error('');
|
|
1186
|
+
this.logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
|
|
1187
|
+
this.logger.error('');
|
|
1074
1188
|
}
|
|
1075
|
-
logger.error('='.repeat(80));
|
|
1076
|
-
logger.error('');
|
|
1189
|
+
this.logger.error('='.repeat(80));
|
|
1190
|
+
this.logger.error('');
|
|
1077
1191
|
} else {
|
|
1078
|
-
logger.error(`[ERROR] Authentication failed: ${errorData?.error || 'Invalid API key'}`);
|
|
1192
|
+
this.logger.error(`[ERROR] Authentication failed: ${errorData?.error || 'Invalid API key'}`);
|
|
1079
1193
|
}
|
|
1080
1194
|
} else if (status !== 403) {
|
|
1081
1195
|
// Log other errors (but not 403 which we handled above)
|
|
1082
|
-
logger.error({
|
|
1196
|
+
this.logger.error({
|
|
1083
1197
|
message: `[ERROR] Failed to send ${payload.type} event to TestLens`,
|
|
1084
1198
|
error: error?.message || 'Unknown error',
|
|
1085
1199
|
status: status,
|
|
@@ -1097,9 +1211,11 @@ export class TestLensReporter implements Reporter {
|
|
|
1097
1211
|
|
|
1098
1212
|
private async processArtifacts(
|
|
1099
1213
|
testId: string,
|
|
1214
|
+
testName: string,
|
|
1100
1215
|
result: TestResult,
|
|
1101
1216
|
testCaseDbId?: string,
|
|
1102
|
-
testEndPayload?: { status: string; endTime: string }
|
|
1217
|
+
testEndPayload?: { status: string; endTime: string },
|
|
1218
|
+
traceNetworkRows?: unknown[]
|
|
1103
1219
|
): Promise<void> {
|
|
1104
1220
|
// Skip artifact processing if run creation failed
|
|
1105
1221
|
if (this.runCreationFailed) {
|
|
@@ -1110,6 +1226,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1110
1226
|
|
|
1111
1227
|
for (const attachment of attachments) {
|
|
1112
1228
|
const artifactType = this.getArtifactType(attachment.name);
|
|
1229
|
+
this.artifactsSeen += 1;
|
|
1113
1230
|
if (attachment.path) {
|
|
1114
1231
|
// Check if attachment should be processed based on config
|
|
1115
1232
|
const isVideo = artifactType === 'video' || attachment.contentType?.startsWith('video/');
|
|
@@ -1117,14 +1234,14 @@ export class TestLensReporter implements Reporter {
|
|
|
1117
1234
|
|
|
1118
1235
|
// Skip video if disabled in config
|
|
1119
1236
|
if (isVideo && !this.config.enableVideo) {
|
|
1120
|
-
logger.debug(`[SKIP] Skipping video artifact ${attachment.name} - video capture disabled in config`);
|
|
1237
|
+
this.logger.debug(`[SKIP] Skipping video artifact ${attachment.name} - video capture disabled in config`);
|
|
1121
1238
|
this.bumpArtifactStat('skipped', artifactType);
|
|
1122
1239
|
continue;
|
|
1123
1240
|
}
|
|
1124
1241
|
|
|
1125
1242
|
// Skip screenshot if disabled in config
|
|
1126
1243
|
if (isScreenshot && !this.config.enableScreenshot) {
|
|
1127
|
-
logger.debug(`[SKIP] Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
|
|
1244
|
+
this.logger.debug(`[SKIP] Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
|
|
1128
1245
|
this.bumpArtifactStat('skipped', artifactType);
|
|
1129
1246
|
continue;
|
|
1130
1247
|
}
|
|
@@ -1182,14 +1299,11 @@ export class TestLensReporter implements Reporter {
|
|
|
1182
1299
|
}
|
|
1183
1300
|
}
|
|
1184
1301
|
const durationMs = Date.now() - traceStart;
|
|
1185
|
-
if (networkRows.length > 0) {
|
|
1186
|
-
|
|
1187
|
-
} else {
|
|
1188
|
-
this.traceNetworkRows = [];
|
|
1302
|
+
if (networkRows.length > 0 && traceNetworkRows) {
|
|
1303
|
+
traceNetworkRows.push(...networkRows);
|
|
1189
1304
|
}
|
|
1190
1305
|
} catch (e) {
|
|
1191
|
-
logger.warn('[TRACE] Could not read trace zip: ' + (e && (e as Error).message));
|
|
1192
|
-
this.traceNetworkRows = [];
|
|
1306
|
+
this.logger.warn('[TRACE] Could not read trace zip: ' + (e && (e as Error).message));
|
|
1193
1307
|
}
|
|
1194
1308
|
}
|
|
1195
1309
|
|
|
@@ -1229,7 +1343,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1229
1343
|
const uploadPromise = Promise.resolve().then(async () => {
|
|
1230
1344
|
try {
|
|
1231
1345
|
if (!attachment.path) {
|
|
1232
|
-
logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
|
|
1346
|
+
this.logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
|
|
1233
1347
|
this.bumpArtifactStat('skipped', artifactType);
|
|
1234
1348
|
return;
|
|
1235
1349
|
}
|
|
@@ -1238,7 +1352,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1238
1352
|
|
|
1239
1353
|
// Skip if upload failed or file was too large
|
|
1240
1354
|
if (!s3Data) {
|
|
1241
|
-
logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
|
|
1355
|
+
this.logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
|
|
1242
1356
|
this.bumpArtifactStat('failed', artifactType);
|
|
1243
1357
|
return;
|
|
1244
1358
|
}
|
|
@@ -1268,9 +1382,14 @@ export class TestLensReporter implements Reporter {
|
|
|
1268
1382
|
artifact: artifactData
|
|
1269
1383
|
});
|
|
1270
1384
|
|
|
1271
|
-
|
|
1385
|
+
// Log artifact upload success at info level
|
|
1386
|
+
this.logger.logArtifactStatus(testName, artifactType, 'uploaded');
|
|
1387
|
+
this.bumpArtifactStat('uploaded', artifactType);
|
|
1388
|
+
this.logger.debug(`[ARTIFACT] [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
|
|
1272
1389
|
} catch (error) {
|
|
1273
|
-
|
|
1390
|
+
// Log artifact upload failure at error level
|
|
1391
|
+
this.logger.logArtifactStatus(testName, artifactType, 'failed');
|
|
1392
|
+
this.logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}: ${(error as Error).message}`);
|
|
1274
1393
|
this.bumpArtifactStat('failed', artifactType);
|
|
1275
1394
|
}
|
|
1276
1395
|
});
|
|
@@ -1284,7 +1403,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1284
1403
|
// Don't await here - let uploads happen in parallel
|
|
1285
1404
|
// They will be awaited in onEnd
|
|
1286
1405
|
} catch (error) {
|
|
1287
|
-
logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}: ${(error as Error).message}`);
|
|
1406
|
+
this.logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}: ${(error as Error).message}`);
|
|
1288
1407
|
this.bumpArtifactStat('failed', artifactType);
|
|
1289
1408
|
}
|
|
1290
1409
|
} else {
|
|
@@ -1293,7 +1412,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1293
1412
|
}
|
|
1294
1413
|
}
|
|
1295
1414
|
|
|
1296
|
-
private async sendSpecCodeBlocks(specPath: string, testName: string, errors: unknown[], runId: string, test_id: string, testCaseId: string): Promise<void> {
|
|
1415
|
+
private async sendSpecCodeBlocks(specPath: string, testName: string, errors: unknown[], runId: string, test_id: string, testCaseId: string, traceNetworkRows: unknown[] = []): Promise<void> {
|
|
1297
1416
|
try {
|
|
1298
1417
|
// Extract code blocks using built-in parser
|
|
1299
1418
|
const testBlocks = this.extractTestBlocks(specPath);
|
|
@@ -1320,24 +1439,24 @@ export class TestLensReporter implements Reporter {
|
|
|
1320
1439
|
filePath: path.relative(process.cwd(), specPath),
|
|
1321
1440
|
codeBlocks,
|
|
1322
1441
|
errors,
|
|
1323
|
-
traceNetworkRows:
|
|
1442
|
+
traceNetworkRows: traceNetworkRows,
|
|
1324
1443
|
testSuiteName: path.basename(specPath).replace(/\.(spec|test)\.(js|ts)$/, ''),
|
|
1325
1444
|
runId,
|
|
1326
1445
|
test_id,
|
|
1327
1446
|
testCaseId
|
|
1328
1447
|
});
|
|
1329
1448
|
|
|
1330
|
-
logger.debug(`Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
|
|
1449
|
+
this.logger.debug(`Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
|
|
1331
1450
|
} catch (error: any) {
|
|
1332
1451
|
const errorData = error?.response?.data;
|
|
1333
1452
|
|
|
1334
1453
|
// Handle duplicate spec code blocks gracefully (when re-running tests)
|
|
1335
1454
|
if (errorData?.error && errorData.error.includes('duplicate key value violates unique constraint')) {
|
|
1336
|
-
logger.debug(`[INFO] Spec code blocks already exist for: ${path.basename(specPath)} (skipped)`);
|
|
1455
|
+
this.logger.debug(`[INFO] Spec code blocks already exist for: ${path.basename(specPath)} (skipped)`);
|
|
1337
1456
|
return;
|
|
1338
1457
|
}
|
|
1339
1458
|
|
|
1340
|
-
logger.error(`Failed to send spec code blocks: ${errorData || error?.message || 'Unknown error'}`);
|
|
1459
|
+
this.logger.error(`Failed to send spec code blocks: ${errorData || error?.message || 'Unknown error'}`);
|
|
1341
1460
|
}
|
|
1342
1461
|
}
|
|
1343
1462
|
|
|
@@ -1444,7 +1563,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1444
1563
|
|
|
1445
1564
|
return blocks;
|
|
1446
1565
|
} catch (error: any) {
|
|
1447
|
-
logger.error(`Failed to extract test blocks from ${filePath}: ${error.message}`);
|
|
1566
|
+
this.logger.error(`Failed to extract test blocks from ${filePath}: ${error.message}`);
|
|
1448
1567
|
return [];
|
|
1449
1568
|
}
|
|
1450
1569
|
}
|
|
@@ -1468,7 +1587,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1468
1587
|
}
|
|
1469
1588
|
} catch (e) {
|
|
1470
1589
|
// Remote info is optional - handle gracefully
|
|
1471
|
-
logger.debug('[INFO] No git remote configured, skipping remote info');
|
|
1590
|
+
this.logger.debug('[INFO] No git remote configured, skipping remote info');
|
|
1472
1591
|
}
|
|
1473
1592
|
|
|
1474
1593
|
const isDirty = execSync('git status --porcelain', { encoding: 'utf-8' }).trim().length > 0;
|
|
@@ -1556,7 +1675,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1556
1675
|
const fileSize = this.getFileSize(filePath);
|
|
1557
1676
|
const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
|
|
1558
1677
|
|
|
1559
|
-
logger.debug(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
|
|
1678
|
+
this.logger.debug(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
|
|
1560
1679
|
|
|
1561
1680
|
const baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
|
|
1562
1681
|
|
|
@@ -1587,7 +1706,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1587
1706
|
const { uploadUrl, s3Key, metadata } = presignedResponse.data;
|
|
1588
1707
|
|
|
1589
1708
|
// Step 2: Upload directly to S3 using presigned URL
|
|
1590
|
-
logger.debug(`[UPLOAD] [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
|
|
1709
|
+
this.logger.debug(`[UPLOAD] [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
|
|
1591
1710
|
|
|
1592
1711
|
const fileBuffer = fs.readFileSync(filePath);
|
|
1593
1712
|
|
|
@@ -1609,7 +1728,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1609
1728
|
throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
|
|
1610
1729
|
}
|
|
1611
1730
|
|
|
1612
|
-
logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
|
|
1731
|
+
this.logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
|
|
1613
1732
|
|
|
1614
1733
|
// Step 3: Confirm upload with server to save metadata
|
|
1615
1734
|
const confirmEndpoint = `${baseUrl}/api/v1/artifacts/public/confirm-upload`;
|
|
@@ -1634,7 +1753,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1634
1753
|
|
|
1635
1754
|
if (confirmResponse.status === 201 && confirmResponse.data.success) {
|
|
1636
1755
|
const artifact = confirmResponse.data.artifact;
|
|
1637
|
-
logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
|
|
1756
|
+
this.logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
|
|
1638
1757
|
return {
|
|
1639
1758
|
key: s3Key,
|
|
1640
1759
|
url: artifact.s3Url,
|
|
@@ -1652,29 +1771,29 @@ export class TestLensReporter implements Reporter {
|
|
|
1652
1771
|
|
|
1653
1772
|
if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
|
|
1654
1773
|
errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
|
|
1655
|
-
logger.error('\n' + '='.repeat(80));
|
|
1774
|
+
this.logger.error('\n' + '='.repeat(80));
|
|
1656
1775
|
|
|
1657
1776
|
if (errorData?.error === 'test_cases_limit_reached') {
|
|
1658
|
-
logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
|
|
1777
|
+
this.logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
|
|
1659
1778
|
} else if (errorData?.error === 'test_runs_limit_reached') {
|
|
1660
|
-
logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
|
|
1779
|
+
this.logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
|
|
1661
1780
|
} else {
|
|
1662
|
-
logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
|
|
1781
|
+
this.logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
|
|
1663
1782
|
}
|
|
1664
1783
|
|
|
1665
|
-
logger.error('='.repeat(80));
|
|
1666
|
-
logger.error('');
|
|
1667
|
-
logger.error(errorData?.message || 'Your trial period has expired.');
|
|
1668
|
-
logger.error('');
|
|
1669
|
-
logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
|
|
1670
|
-
logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
|
|
1671
|
-
logger.error('');
|
|
1784
|
+
this.logger.error('='.repeat(80));
|
|
1785
|
+
this.logger.error('');
|
|
1786
|
+
this.logger.error(errorData?.message || 'Your trial period has expired.');
|
|
1787
|
+
this.logger.error('');
|
|
1788
|
+
this.logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
|
|
1789
|
+
this.logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
|
|
1790
|
+
this.logger.error('');
|
|
1672
1791
|
if (errorData?.trial_end_date) {
|
|
1673
|
-
logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
|
|
1674
|
-
logger.error('');
|
|
1792
|
+
this.logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
|
|
1793
|
+
this.logger.error('');
|
|
1675
1794
|
}
|
|
1676
|
-
logger.error('='.repeat(80));
|
|
1677
|
-
logger.error('');
|
|
1795
|
+
this.logger.error('='.repeat(80));
|
|
1796
|
+
this.logger.error('');
|
|
1678
1797
|
return null;
|
|
1679
1798
|
}
|
|
1680
1799
|
}
|
|
@@ -1692,9 +1811,9 @@ export class TestLensReporter implements Reporter {
|
|
|
1692
1811
|
errorMsg = `Access denied (403) - presigned URL may have expired`;
|
|
1693
1812
|
}
|
|
1694
1813
|
|
|
1695
|
-
logger.error(`[ERROR] Failed to upload ${fileName} to S3: ${errorMsg}`);
|
|
1814
|
+
this.logger.error(`[ERROR] Failed to upload ${fileName} to S3: ${errorMsg}`);
|
|
1696
1815
|
if (error.response?.data) {
|
|
1697
|
-
logger.error({ errorDetails: error.response.data }, 'Error details');
|
|
1816
|
+
this.logger.error({ errorDetails: error.response.data }, 'Error details');
|
|
1698
1817
|
}
|
|
1699
1818
|
|
|
1700
1819
|
// Don't throw, just return null to continue with other artifacts
|
|
@@ -1741,7 +1860,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1741
1860
|
const stats = fs.statSync(filePath);
|
|
1742
1861
|
return stats.size;
|
|
1743
1862
|
} catch (error) {
|
|
1744
|
-
logger.warn(`Could not get file size for ${filePath}: ${(error as Error).message}`);
|
|
1863
|
+
this.logger.warn(`Could not get file size for ${filePath}: ${(error as Error).message}`);
|
|
1745
1864
|
return 0;
|
|
1746
1865
|
}
|
|
1747
1866
|
}
|