@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.js
CHANGED
|
@@ -10,65 +10,135 @@ const https = tslib_1.__importStar(require("https"));
|
|
|
10
10
|
const axios_1 = tslib_1.__importDefault(require("axios"));
|
|
11
11
|
const child_process_1 = require("child_process");
|
|
12
12
|
const pino_1 = tslib_1.__importDefault(require("pino"));
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
'Uploading ',
|
|
33
|
-
'Processed artifact',
|
|
34
|
-
'Spec code blocks',
|
|
35
|
-
'\n[METADATA]',
|
|
36
|
-
'Custom Metadata Detected',
|
|
37
|
-
'Using API key from environment variable'
|
|
38
|
-
];
|
|
39
|
-
const shouldSuppressInfo = (msg) => {
|
|
40
|
-
if (isVerboseLogs)
|
|
41
|
-
return false;
|
|
42
|
-
if (!msg)
|
|
43
|
-
return false;
|
|
44
|
-
return (noisyInfoPrefixes.some(prefix => msg.startsWith(prefix)) ||
|
|
45
|
-
noisyInfoSubstrings.some(sub => msg.includes(sub)));
|
|
46
|
-
};
|
|
47
|
-
// Create pino logger instance
|
|
48
|
-
const logger = (0, pino_1.default)({
|
|
49
|
-
level: process.env.TESTLENS_LOG_LEVEL || process.env.LOG_LEVEL || 'info',
|
|
50
|
-
base: null, // remove pid/hostname
|
|
51
|
-
timestamp: false, // remove time
|
|
52
|
-
formatters: {
|
|
53
|
-
level: () => ({}) // remove level
|
|
54
|
-
},
|
|
55
|
-
hooks: {
|
|
56
|
-
logMethod(args, method, level) {
|
|
57
|
-
// pino level is numeric (info = 30)
|
|
58
|
-
if (level === 30) {
|
|
59
|
-
const msg = typeof args[0] === 'string'
|
|
60
|
-
? args[0]
|
|
61
|
-
: typeof args[1] === 'string'
|
|
62
|
-
? args[1]
|
|
63
|
-
: '';
|
|
64
|
-
if (shouldSuppressInfo(msg)) {
|
|
65
|
-
return;
|
|
13
|
+
const TESTLENS_X_WORKFLOW_AUTH = 'd5f8f1d1a4546e9b49dcedbe51f483aca4bb0e2357b513082b0ce49e59583d38';
|
|
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
|
+
constructor(logLevel = 'info') {
|
|
23
|
+
this.logLevel = logLevel;
|
|
24
|
+
// Create base pino logger with CloudWatch-compatible settings
|
|
25
|
+
this.pinoLogger = (0, pino_1.default)({
|
|
26
|
+
level: 'debug', // Always log at debug level, we filter manually
|
|
27
|
+
base: null, // remove pid/hostname - CloudWatch provides this
|
|
28
|
+
timestamp: false, // CloudWatch adds timestamps
|
|
29
|
+
formatters: {
|
|
30
|
+
level: (label) => {
|
|
31
|
+
return { level: label }; // Include level for CloudWatch filtering
|
|
66
32
|
}
|
|
67
33
|
}
|
|
68
|
-
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Set run ID context for all subsequent logs
|
|
38
|
+
* This allows correlating all logs to a specific test run in CloudWatch
|
|
39
|
+
*/
|
|
40
|
+
setRunId(runId) {
|
|
41
|
+
this.runId = runId;
|
|
42
|
+
// Recreate logger with runId context for CloudWatch querying
|
|
43
|
+
this.pinoLogger = this.pinoLogger.child({ runId });
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Check if we're in debug mode
|
|
47
|
+
*/
|
|
48
|
+
isDebug() {
|
|
49
|
+
return this.logLevel === 'debug';
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Debug level logging - only shown when logLevel is 'debug'
|
|
53
|
+
*/
|
|
54
|
+
debug(msg, ...args) {
|
|
55
|
+
if (this.logLevel === 'debug') {
|
|
56
|
+
this.pinoLogger.debug(msg, ...args);
|
|
69
57
|
}
|
|
70
58
|
}
|
|
71
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Debug level logging with object
|
|
61
|
+
*/
|
|
62
|
+
debugObj(obj, msg) {
|
|
63
|
+
if (this.logLevel === 'debug') {
|
|
64
|
+
this.pinoLogger.debug(obj, msg);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Info level logging - shown for both 'info' and 'debug' levels
|
|
69
|
+
*/
|
|
70
|
+
info(msg, ...args) {
|
|
71
|
+
this.pinoLogger.info(msg, ...args);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Info level logging with object
|
|
75
|
+
*/
|
|
76
|
+
infoObj(obj, msg) {
|
|
77
|
+
this.pinoLogger.info(obj, msg);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Warn level logging - shown for both 'info' and 'debug' levels
|
|
81
|
+
*/
|
|
82
|
+
warn(msg, ...args) {
|
|
83
|
+
this.pinoLogger.warn(msg, ...args);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Error level logging - always shown regardless of log level
|
|
87
|
+
*/
|
|
88
|
+
error(msg, ...args) {
|
|
89
|
+
if (typeof msg === 'string') {
|
|
90
|
+
this.pinoLogger.error(msg, ...args);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
this.pinoLogger.error(msg, ...args);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Log test case start - debug level only
|
|
98
|
+
* In info mode, we only show logs when there are issues
|
|
99
|
+
*/
|
|
100
|
+
logTestStart(testName) {
|
|
101
|
+
this.debug(`[TEST START] ${testName}`);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Log test case completion - debug level only
|
|
105
|
+
* In info mode, we only show logs when there are issues
|
|
106
|
+
*/
|
|
107
|
+
logTestEnd(testName, status, duration) {
|
|
108
|
+
const statusEmoji = status === 'passed' ? '✓' : status === 'failed' ? '✗' : '○';
|
|
109
|
+
const durationStr = `${duration}ms`;
|
|
110
|
+
this.debug(`[TEST END] ${statusEmoji} ${testName} - ${status} (${durationStr})`);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Log artifact status - info level only for failures
|
|
114
|
+
* Success is logged at debug level
|
|
115
|
+
*/
|
|
116
|
+
logArtifactStatus(testName, artifactType, status) {
|
|
117
|
+
if (status === 'failed') {
|
|
118
|
+
this.error(`[ARTIFACT] ✗ ${artifactType} for "${testName}" - failed`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
this.debug(`[ARTIFACT] ${status === 'uploaded' ? '✓' : '○'} ${artifactType} for "${testName}" - ${status}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Log run summary - debug level only
|
|
126
|
+
* In info mode, we only show logs when there are issues
|
|
127
|
+
*/
|
|
128
|
+
logRunSummary(passed, failed, skipped, timedOut) {
|
|
129
|
+
this.debug(`[RUN SUMMARY] ${passed} passed, ${failed} failed (${timedOut} timeouts), ${skipped} skipped`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Legacy logger for backward compatibility during transition
|
|
133
|
+
// Will be replaced with Logger instance per reporter
|
|
134
|
+
let globalLogger = new Logger('info');
|
|
135
|
+
const logger = {
|
|
136
|
+
get debug() { return globalLogger.debug.bind(globalLogger); },
|
|
137
|
+
get info() { return globalLogger.info.bind(globalLogger); },
|
|
138
|
+
get warn() { return globalLogger.warn.bind(globalLogger); },
|
|
139
|
+
get error() { return globalLogger.error.bind(globalLogger); },
|
|
140
|
+
get levelVal() { return globalLogger.isDebug() ? 20 : 30; }
|
|
141
|
+
};
|
|
72
142
|
// Lazy-load mime module to support ESM
|
|
73
143
|
let mimeModule = null;
|
|
74
144
|
async function getMime() {
|
|
@@ -80,6 +150,17 @@ async function getMime() {
|
|
|
80
150
|
return mimeModule;
|
|
81
151
|
}
|
|
82
152
|
class TestLensReporter {
|
|
153
|
+
/**
|
|
154
|
+
* Get environment variable value with case-insensitive key match (e.g. TL_BUILDNAME, tl_buildname).
|
|
155
|
+
*/
|
|
156
|
+
static getEnvCaseInsensitive(name) {
|
|
157
|
+
const upper = name.toUpperCase();
|
|
158
|
+
for (const key of Object.keys(process.env)) {
|
|
159
|
+
if (key.toUpperCase() === upper)
|
|
160
|
+
return process.env[key];
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
83
164
|
/**
|
|
84
165
|
* Parse custom metadata from environment variables
|
|
85
166
|
* Checks for common metadata environment variables
|
|
@@ -88,11 +169,12 @@ class TestLensReporter {
|
|
|
88
169
|
const customArgs = {};
|
|
89
170
|
// Common environment variable names for build metadata
|
|
90
171
|
const envVarMappings = {
|
|
91
|
-
// Support both TestLens-specific names (recommended) and common CI names
|
|
92
|
-
'testlensBuildTag': ['testlensBuildTag', 'TESTLENS_BUILD_TAG', 'TESTLENS_BUILDTAG', 'BUILDTAG', 'BUILD_TAG', 'TestlensBuildTag', 'TestLensBuildTag'],
|
|
93
|
-
'testlensBuildName': ['testlensBuildName', 'TESTLENS_BUILD_NAME', 'TESTLENS_BUILDNAME', 'BUILDNAME', 'BUILD_NAME', 'TestlensBuildName', 'TestLensBuildName'],
|
|
172
|
+
// Support both TestLens-specific names (recommended) and common CI names; TL_* are short aliases; keys matched case-insensitively
|
|
173
|
+
'testlensBuildTag': ['TL_BUILDTAG', 'testlensBuildTag', 'TESTLENS_BUILD_TAG', 'TESTLENS_BUILDTAG', 'BUILDTAG', 'BUILD_TAG', 'TestlensBuildTag', 'TestLensBuildTag'],
|
|
174
|
+
'testlensBuildName': ['TL_BUILDNAME', 'testlensBuildName', 'TESTLENS_BUILD_NAME', 'TESTLENS_BUILDNAME', 'BUILDNAME', 'BUILD_NAME', 'TestlensBuildName', 'TestLensBuildName'],
|
|
94
175
|
// Execution ID for one run per pipeline (checked in order; prefer TESTLENS_EXECUTION_ID, then CI-specific UUIDs)
|
|
95
176
|
'executionId': [
|
|
177
|
+
'TL_EXECUTIONID',
|
|
96
178
|
'TESTLENS_EXECUTION_ID',
|
|
97
179
|
'TestlensExecutionId',
|
|
98
180
|
'TestLensExecutionId',
|
|
@@ -114,10 +196,10 @@ class TestLensReporter {
|
|
|
114
196
|
'project': ['PROJECT', 'PROJECT_NAME'],
|
|
115
197
|
'customvalue': ['CUSTOMVALUE', 'CUSTOM_VALUE']
|
|
116
198
|
};
|
|
117
|
-
// Check for each metadata key
|
|
199
|
+
// Check for each metadata key (case-insensitive env lookup)
|
|
118
200
|
Object.entries(envVarMappings).forEach(([key, envVars]) => {
|
|
119
201
|
for (const envVar of envVars) {
|
|
120
|
-
const value =
|
|
202
|
+
const value = TestLensReporter.getEnvCaseInsensitive(envVar);
|
|
121
203
|
if (value) {
|
|
122
204
|
// For testlensBuildTag, support comma-separated values
|
|
123
205
|
if (key === 'testlensBuildTag' && value.includes(',')) {
|
|
@@ -139,13 +221,20 @@ class TestLensReporter {
|
|
|
139
221
|
this.runCreationFailed = false; // Track if run creation failed due to limits
|
|
140
222
|
this.cliArgs = {}; // Store CLI args separately
|
|
141
223
|
this.pendingUploads = new Set(); // Track pending artifact uploads
|
|
142
|
-
this.traceNetworkRows = []; // Network requests/responses from trace zip for current test
|
|
143
224
|
this.artifactStats = {
|
|
144
225
|
uploaded: { screenshot: 0, video: 0, trace: 0, attachment: 0 },
|
|
145
226
|
skipped: { screenshot: 0, video: 0, trace: 0, attachment: 0 },
|
|
146
227
|
failed: { screenshot: 0, video: 0, trace: 0, attachment: 0 }
|
|
147
228
|
};
|
|
148
229
|
this.artifactsSeen = 0;
|
|
230
|
+
// Initialize logger first with user-configured log level
|
|
231
|
+
const logLevel = options.logLevel ||
|
|
232
|
+
process.env.TESTLENS_LOG_LEVEL ||
|
|
233
|
+
(process.env.LOG_LEVEL === 'debug' ? 'debug' : 'info') ||
|
|
234
|
+
'info';
|
|
235
|
+
this.logger = new Logger(logLevel);
|
|
236
|
+
// Update global logger reference for any legacy code
|
|
237
|
+
globalLogger = this.logger;
|
|
149
238
|
// Parse custom CLI arguments
|
|
150
239
|
const customArgs = TestLensReporter.parseCustomArgs();
|
|
151
240
|
this.cliArgs = customArgs; // Store CLI args separately for later use
|
|
@@ -176,13 +265,14 @@ class TestLensReporter {
|
|
|
176
265
|
rejectUnauthorized: options.rejectUnauthorized,
|
|
177
266
|
ignoreSslErrors: options.ignoreSslErrors,
|
|
178
267
|
customMetadata: { ...options.customMetadata, ...customArgs }, // Config metadata first, then CLI args override
|
|
179
|
-
executionId: options.executionId
|
|
268
|
+
executionId: options.executionId,
|
|
269
|
+
logLevel: logLevel
|
|
180
270
|
};
|
|
181
271
|
if (!this.config.apiKey) {
|
|
182
272
|
throw new Error('API_KEY is required for TestLensReporter. Pass it as apiKey option in your playwright config or set one of these environment variables: TESTLENS_API_KEY, TESTLENS_KEY, PLAYWRIGHT_API_KEY, PW_API_KEY, API_KEY, or APIKEY.');
|
|
183
273
|
}
|
|
184
274
|
if (apiKey !== options.apiKey) {
|
|
185
|
-
logger.debug('✓ Using API key from environment variable');
|
|
275
|
+
this.logger.debug('✓ Using API key from environment variable');
|
|
186
276
|
}
|
|
187
277
|
// Default environment to allow self-signed certs unless explicitly set
|
|
188
278
|
if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === undefined) {
|
|
@@ -193,18 +283,18 @@ class TestLensReporter {
|
|
|
193
283
|
// Check various ways SSL validation can be disabled or enforced (in order of precedence)
|
|
194
284
|
if (this.config.ignoreSslErrors === true) {
|
|
195
285
|
rejectUnauthorized = false;
|
|
196
|
-
logger.debug('[DEBUG] SSL certificate validation disabled via ignoreSslErrors option');
|
|
286
|
+
this.logger.debug('[DEBUG] SSL certificate validation disabled via ignoreSslErrors option');
|
|
197
287
|
}
|
|
198
288
|
else if (this.config.rejectUnauthorized === false) {
|
|
199
289
|
rejectUnauthorized = false;
|
|
200
|
-
logger.debug('[DEBUG] SSL certificate validation disabled via rejectUnauthorized option');
|
|
290
|
+
this.logger.debug('[DEBUG] SSL certificate validation disabled via rejectUnauthorized option');
|
|
201
291
|
}
|
|
202
292
|
else if (this.config.rejectUnauthorized === true) {
|
|
203
293
|
rejectUnauthorized = true;
|
|
204
294
|
}
|
|
205
295
|
else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') {
|
|
206
296
|
rejectUnauthorized = false;
|
|
207
|
-
logger.debug('[DEBUG] SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
|
|
297
|
+
this.logger.debug('[DEBUG] SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
|
|
208
298
|
}
|
|
209
299
|
else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '1') {
|
|
210
300
|
rejectUnauthorized = true;
|
|
@@ -217,6 +307,7 @@ class TestLensReporter {
|
|
|
217
307
|
timeout: this.config.timeout,
|
|
218
308
|
headers: {
|
|
219
309
|
'Content-Type': 'application/json',
|
|
310
|
+
'x-workflow-auth': TESTLENS_X_WORKFLOW_AUTH,
|
|
220
311
|
...(this.config.apiKey && { 'X-API-Key': this.config.apiKey }),
|
|
221
312
|
},
|
|
222
313
|
// Enhanced SSL handling with flexible TLS configuration
|
|
@@ -253,17 +344,19 @@ class TestLensReporter {
|
|
|
253
344
|
const executionId = typeof executionIdRaw === 'string' && executionIdRaw.trim() ? String(executionIdRaw).trim() : undefined;
|
|
254
345
|
this.runId = executionId || (0, crypto_1.randomUUID)();
|
|
255
346
|
this.usedExecutionId = !!executionId;
|
|
347
|
+
// Set runId in logger context for CloudWatch correlation
|
|
348
|
+
this.logger.setRunId(this.runId);
|
|
256
349
|
this.runMetadata = this.initializeRunMetadata();
|
|
257
350
|
this.specMap = new Map();
|
|
258
351
|
this.testMap = new Map();
|
|
259
352
|
this.runCreationFailed = false;
|
|
260
353
|
// Log custom metadata if any
|
|
261
354
|
if (this.config.customMetadata && Object.keys(this.config.customMetadata).length > 0) {
|
|
262
|
-
logger.debug('\n[METADATA] Custom Metadata Detected:');
|
|
355
|
+
this.logger.debug('\n[METADATA] Custom Metadata Detected:');
|
|
263
356
|
Object.entries(this.config.customMetadata).forEach(([key, value]) => {
|
|
264
|
-
logger.debug(` ${key}: ${value}`);
|
|
357
|
+
this.logger.debug(` ${key}: ${value}`);
|
|
265
358
|
});
|
|
266
|
-
logger.debug('');
|
|
359
|
+
this.logger.debug('');
|
|
267
360
|
}
|
|
268
361
|
}
|
|
269
362
|
initializeRunMetadata() {
|
|
@@ -326,27 +419,21 @@ class TestLensReporter {
|
|
|
326
419
|
return status;
|
|
327
420
|
}
|
|
328
421
|
async onBegin(config, suite) {
|
|
329
|
-
logger.debug(`[REPORTER] source=${__filename} enableArtifacts=${this.config.enableArtifacts}`);
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
logger.debug(`TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
|
|
333
|
-
logger.debug(` Run ID: ${this.runId}`);
|
|
334
|
-
}
|
|
335
|
-
else {
|
|
336
|
-
logger.debug(`TestLens Reporter starting - Run ID: ${this.runId}`);
|
|
337
|
-
}
|
|
422
|
+
this.logger.debug(`[REPORTER] source=${__filename} enableArtifacts=${this.config.enableArtifacts}`);
|
|
423
|
+
// Only show startup info in debug mode - in info mode we only log issues
|
|
424
|
+
this.logger.debug(`TestLens Reporter starting - Run ID: ${this.runId}`);
|
|
338
425
|
// Collect Git information if enabled
|
|
339
426
|
if (this.config.enableGitInfo) {
|
|
340
427
|
this.runMetadata.gitInfo = await this.collectGitInfo();
|
|
341
428
|
if (this.runMetadata.gitInfo) {
|
|
342
|
-
logger.debug(`Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
|
|
429
|
+
this.logger.debug(`Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
|
|
343
430
|
}
|
|
344
431
|
else {
|
|
345
|
-
logger.warn(`[WARN] Git info collection returned null - not in a git repository or git not available`);
|
|
432
|
+
this.logger.warn(`[WARN] Git info collection returned null - not in a git repository or git not available`);
|
|
346
433
|
}
|
|
347
434
|
}
|
|
348
435
|
else {
|
|
349
|
-
logger.debug(`[INFO] Git info collection disabled (enableGitInfo: false)`);
|
|
436
|
+
this.logger.debug(`[INFO] Git info collection disabled (enableGitInfo: false)`);
|
|
350
437
|
}
|
|
351
438
|
// Add shard information if available
|
|
352
439
|
if (config.shard) {
|
|
@@ -364,8 +451,8 @@ class TestLensReporter {
|
|
|
364
451
|
});
|
|
365
452
|
}
|
|
366
453
|
async onTestBegin(test, result) {
|
|
367
|
-
// Log which test is starting
|
|
368
|
-
logger.
|
|
454
|
+
// Log which test is starting (info level for test start)
|
|
455
|
+
this.logger.logTestStart(test.title);
|
|
369
456
|
const specPath = test.location.file;
|
|
370
457
|
const specKey = `${specPath}-${test.parent.title}`;
|
|
371
458
|
// Create or update spec data
|
|
@@ -436,27 +523,14 @@ class TestLensReporter {
|
|
|
436
523
|
}
|
|
437
524
|
}
|
|
438
525
|
async onTestEnd(test, result) {
|
|
439
|
-
this.
|
|
526
|
+
this.logger.debug(`[ARTIFACTS] attachments=${result.attachments?.length ?? 0}`);
|
|
440
527
|
const testId = this.getTestId(test);
|
|
441
528
|
let testCaseId = '';
|
|
529
|
+
const localTraceNetworkRows = [];
|
|
442
530
|
let testData = this.testMap.get(testId);
|
|
443
|
-
logger.debug(`[ARTIFACTS] attachments=${result.attachments?.length ?? 0}`);
|
|
444
|
-
if (result.attachments && result.attachments.length > 0) {
|
|
445
|
-
for (const attachment of result.attachments) {
|
|
446
|
-
const artifactType = this.getArtifactType(attachment.name);
|
|
447
|
-
this.artifactsSeen += 1;
|
|
448
|
-
if (attachment.path) {
|
|
449
|
-
this.bumpArtifactStat('uploaded', artifactType);
|
|
450
|
-
}
|
|
451
|
-
else {
|
|
452
|
-
this.bumpArtifactStat('skipped', artifactType);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
logger.debug(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
|
|
457
531
|
// For skipped tests, onTestBegin might not be called, so we need to create the test data here
|
|
458
532
|
if (!testData) {
|
|
459
|
-
logger.debug(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
|
|
533
|
+
this.logger.debug(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
|
|
460
534
|
// Create spec data if not exists (skipped tests might not have spec data either)
|
|
461
535
|
const specPath = test.location.file;
|
|
462
536
|
const specKey = `${specPath}-${test.parent.title}`;
|
|
@@ -601,7 +675,7 @@ class TestLensReporter {
|
|
|
601
675
|
});
|
|
602
676
|
// Send testEnd event for all tests, regardless of status
|
|
603
677
|
// This ensures tests that are interrupted or have unexpected statuses are properly recorded
|
|
604
|
-
logger.debug(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
|
|
678
|
+
this.logger.debug(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
|
|
605
679
|
// Send test end event to API and get response
|
|
606
680
|
const testEndResponse = await this.sendToApi({
|
|
607
681
|
type: 'testEnd',
|
|
@@ -610,14 +684,16 @@ class TestLensReporter {
|
|
|
610
684
|
test: testData
|
|
611
685
|
});
|
|
612
686
|
testCaseId = testEndResponse?.testCaseId;
|
|
687
|
+
// Log test completion with status (info level)
|
|
688
|
+
this.logger.logTestEnd(test.title, testData.status, testData.duration);
|
|
613
689
|
// Handle artifacts (test case is now guaranteed to be in database)
|
|
614
690
|
if (this.config.enableArtifacts) {
|
|
615
691
|
// Pass test case DB ID if available for faster lookups; pass status/endTime so backend
|
|
616
692
|
// can fix up test case status if testEnd failed but artifact request succeeds
|
|
617
|
-
await this.processArtifacts(testId, result, testEndResponse?.testCaseId, {
|
|
693
|
+
await this.processArtifacts(testId, test.title, result, testEndResponse?.testCaseId, {
|
|
618
694
|
status: testData.status,
|
|
619
695
|
endTime: testData.endTime
|
|
620
|
-
});
|
|
696
|
+
}, localTraceNetworkRows);
|
|
621
697
|
}
|
|
622
698
|
else if (result.attachments && result.attachments.length > 0) {
|
|
623
699
|
for (const attachment of result.attachments) {
|
|
@@ -687,7 +763,7 @@ class TestLensReporter {
|
|
|
687
763
|
spec: specData
|
|
688
764
|
});
|
|
689
765
|
// Send spec code blocks to API
|
|
690
|
-
await this.sendSpecCodeBlocks(specPath, test.title, testData?.errors || [], this.runId, testId, testCaseId);
|
|
766
|
+
await this.sendSpecCodeBlocks(specPath, test.title, testData?.errors || [], this.runId, testId, testCaseId, localTraceNetworkRows);
|
|
691
767
|
}
|
|
692
768
|
}
|
|
693
769
|
}
|
|
@@ -696,29 +772,32 @@ class TestLensReporter {
|
|
|
696
772
|
this.runMetadata.duration = Date.now() - new Date(this.runMetadata.startTime).getTime();
|
|
697
773
|
// Wait for all pending artifact uploads to complete before sending runEnd
|
|
698
774
|
if (this.pendingUploads.size > 0) {
|
|
699
|
-
logger.debug(`Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
|
|
775
|
+
this.logger.debug(`Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
|
|
700
776
|
try {
|
|
701
777
|
await Promise.all(Array.from(this.pendingUploads));
|
|
702
|
-
logger.debug(`[OK] All artifact uploads completed`);
|
|
778
|
+
this.logger.debug(`[OK] All artifact uploads completed`);
|
|
703
779
|
}
|
|
704
780
|
catch (error) {
|
|
705
|
-
logger.error(`[WARN] Some artifact uploads failed, continuing with runEnd`);
|
|
781
|
+
this.logger.error(`[WARN] Some artifact uploads failed, continuing with runEnd`);
|
|
706
782
|
}
|
|
707
783
|
}
|
|
708
784
|
const uploaded = this.artifactStats.uploaded;
|
|
709
785
|
const skipped = this.artifactStats.skipped;
|
|
710
786
|
const failed = this.artifactStats.failed;
|
|
711
|
-
//
|
|
787
|
+
// Calculate total uploaded/failed for info level summary
|
|
788
|
+
const totalUploaded = uploaded.screenshot + uploaded.video + uploaded.trace + uploaded.attachment;
|
|
789
|
+
const totalFailed = failed.screenshot + failed.video + failed.trace + failed.attachment;
|
|
790
|
+
const totalSkipped = skipped.screenshot + skipped.video + skipped.trace + skipped.attachment;
|
|
791
|
+
// Info level: only show artifact summary if there are failures
|
|
792
|
+
if (totalFailed > 0) {
|
|
793
|
+
this.logger.error(`[ARTIFACTS] ${totalUploaded} uploaded, ${totalFailed} failed, ${totalSkipped} skipped`);
|
|
794
|
+
}
|
|
795
|
+
// Debug level: show detailed breakdown
|
|
712
796
|
const summary = `[ARTIFACTS] seen=${this.artifactsSeen} | ` +
|
|
713
797
|
`uploaded screenshot=${uploaded.screenshot}, video=${uploaded.video}, trace=${uploaded.trace}, attachment=${uploaded.attachment} | ` +
|
|
714
798
|
`skipped screenshot=${skipped.screenshot}, video=${skipped.video}, trace=${skipped.trace}, attachment=${skipped.attachment} | ` +
|
|
715
799
|
`failed screenshot=${failed.screenshot}, video=${failed.video}, trace=${failed.trace}, attachment=${failed.attachment}`;
|
|
716
|
-
|
|
717
|
-
console.log(summary);
|
|
718
|
-
}
|
|
719
|
-
else {
|
|
720
|
-
logger.debug(summary);
|
|
721
|
-
}
|
|
800
|
+
this.logger.debug(summary);
|
|
722
801
|
// Calculate final stats
|
|
723
802
|
const totalTests = Array.from(this.testMap.values()).length;
|
|
724
803
|
const passedTests = Array.from(this.testMap.values()).filter(t => t.status === 'passed').length;
|
|
@@ -746,15 +825,10 @@ class TestLensReporter {
|
|
|
746
825
|
aggregationMode: this.usedExecutionId ? 'append' : 'replace'
|
|
747
826
|
}
|
|
748
827
|
});
|
|
749
|
-
//
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
}
|
|
754
|
-
else {
|
|
755
|
-
logger.debug(`[COMPLETE] TestLens Report completed - Run ID: ${this.runId}`);
|
|
756
|
-
}
|
|
757
|
-
logger.debug(`[RESULTS] ${passedTests} passed, ${failedTests} failed (${timedOutTests} timeouts), ${skippedTests} skipped`);
|
|
828
|
+
// Only show completion info in debug mode - in info mode we only log issues
|
|
829
|
+
this.logger.debug(`[COMPLETE] TestLens Report completed - Run ID: ${this.runId}`);
|
|
830
|
+
// Log run summary at debug level
|
|
831
|
+
this.logger.logRunSummary(passedTests, failedTests, skippedTests, timedOutTests);
|
|
758
832
|
}
|
|
759
833
|
async sendToApi(payload) {
|
|
760
834
|
// Skip sending if run creation already failed
|
|
@@ -768,7 +842,7 @@ class TestLensReporter {
|
|
|
768
842
|
}
|
|
769
843
|
});
|
|
770
844
|
if (this.config.enableRealTimeStream) {
|
|
771
|
-
logger.debug(`[OK] Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
|
|
845
|
+
this.logger.debug(`[OK] Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
|
|
772
846
|
}
|
|
773
847
|
// Return response data for caller to use
|
|
774
848
|
return response.data;
|
|
@@ -782,64 +856,64 @@ class TestLensReporter {
|
|
|
782
856
|
if (payload.type === 'runStart' && errorData?.limit_type === 'test_runs') {
|
|
783
857
|
this.runCreationFailed = true;
|
|
784
858
|
}
|
|
785
|
-
logger.error('\n' + '='.repeat(80));
|
|
859
|
+
this.logger.error('\n' + '='.repeat(80));
|
|
786
860
|
if (errorData?.limit_type === 'test_cases') {
|
|
787
|
-
logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
|
|
861
|
+
this.logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
|
|
788
862
|
}
|
|
789
863
|
else if (errorData?.limit_type === 'test_runs') {
|
|
790
|
-
logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
|
|
864
|
+
this.logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
|
|
791
865
|
}
|
|
792
866
|
else {
|
|
793
|
-
logger.error('[ERROR] TESTLENS ERROR: Plan Limit Reached');
|
|
867
|
+
this.logger.error('[ERROR] TESTLENS ERROR: Plan Limit Reached');
|
|
794
868
|
}
|
|
795
|
-
logger.error('='.repeat(80));
|
|
796
|
-
logger.error('');
|
|
797
|
-
logger.error(errorData?.message || 'You have reached your plan limit.');
|
|
798
|
-
logger.error('');
|
|
799
|
-
logger.error(`Current usage: ${errorData?.current_usage || 'N/A'} / ${errorData?.limit || 'N/A'}`);
|
|
800
|
-
logger.error('');
|
|
801
|
-
logger.error('To continue, please upgrade your plan.');
|
|
802
|
-
logger.error('Contact: support@alternative-path.com');
|
|
803
|
-
logger.error('');
|
|
804
|
-
logger.error('='.repeat(80));
|
|
805
|
-
logger.error('');
|
|
869
|
+
this.logger.error('='.repeat(80));
|
|
870
|
+
this.logger.error('');
|
|
871
|
+
this.logger.error(errorData?.message || 'You have reached your plan limit.');
|
|
872
|
+
this.logger.error('');
|
|
873
|
+
this.logger.error(`Current usage: ${errorData?.current_usage || 'N/A'} / ${errorData?.limit || 'N/A'}`);
|
|
874
|
+
this.logger.error('');
|
|
875
|
+
this.logger.error('To continue, please upgrade your plan.');
|
|
876
|
+
this.logger.error('Contact: support@alternative-path.com');
|
|
877
|
+
this.logger.error('');
|
|
878
|
+
this.logger.error('='.repeat(80));
|
|
879
|
+
this.logger.error('');
|
|
806
880
|
return; // Don't log the full error object for limit errors
|
|
807
881
|
}
|
|
808
882
|
// Check for trial expiration, subscription errors, or limit errors (401)
|
|
809
883
|
if (status === 401) {
|
|
810
884
|
if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
|
|
811
885
|
errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
|
|
812
|
-
logger.error('\n' + '='.repeat(80));
|
|
886
|
+
this.logger.error('\n' + '='.repeat(80));
|
|
813
887
|
if (errorData?.error === 'test_cases_limit_reached') {
|
|
814
|
-
logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
|
|
888
|
+
this.logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
|
|
815
889
|
}
|
|
816
890
|
else if (errorData?.error === 'test_runs_limit_reached') {
|
|
817
|
-
logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
|
|
891
|
+
this.logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
|
|
818
892
|
}
|
|
819
893
|
else {
|
|
820
|
-
logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
|
|
894
|
+
this.logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
|
|
821
895
|
}
|
|
822
|
-
logger.error('='.repeat(80));
|
|
823
|
-
logger.error('');
|
|
824
|
-
logger.error(errorData?.message || 'Your trial period has expired.');
|
|
825
|
-
logger.error('');
|
|
826
|
-
logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
|
|
827
|
-
logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
|
|
828
|
-
logger.error('');
|
|
896
|
+
this.logger.error('='.repeat(80));
|
|
897
|
+
this.logger.error('');
|
|
898
|
+
this.logger.error(errorData?.message || 'Your trial period has expired.');
|
|
899
|
+
this.logger.error('');
|
|
900
|
+
this.logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
|
|
901
|
+
this.logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
|
|
902
|
+
this.logger.error('');
|
|
829
903
|
if (errorData?.trial_end_date) {
|
|
830
|
-
logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
|
|
831
|
-
logger.error('');
|
|
904
|
+
this.logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
|
|
905
|
+
this.logger.error('');
|
|
832
906
|
}
|
|
833
|
-
logger.error('='.repeat(80));
|
|
834
|
-
logger.error('');
|
|
907
|
+
this.logger.error('='.repeat(80));
|
|
908
|
+
this.logger.error('');
|
|
835
909
|
}
|
|
836
910
|
else {
|
|
837
|
-
logger.error(`[ERROR] Authentication failed: ${errorData?.error || 'Invalid API key'}`);
|
|
911
|
+
this.logger.error(`[ERROR] Authentication failed: ${errorData?.error || 'Invalid API key'}`);
|
|
838
912
|
}
|
|
839
913
|
}
|
|
840
914
|
else if (status !== 403) {
|
|
841
915
|
// Log other errors (but not 403 which we handled above)
|
|
842
|
-
logger.error({
|
|
916
|
+
this.logger.error({
|
|
843
917
|
message: `[ERROR] Failed to send ${payload.type} event to TestLens`,
|
|
844
918
|
error: error?.message || 'Unknown error',
|
|
845
919
|
status: status,
|
|
@@ -853,7 +927,7 @@ class TestLensReporter {
|
|
|
853
927
|
// Don't throw error to avoid breaking test execution
|
|
854
928
|
}
|
|
855
929
|
}
|
|
856
|
-
async processArtifacts(testId, result, testCaseDbId, testEndPayload) {
|
|
930
|
+
async processArtifacts(testId, testName, result, testCaseDbId, testEndPayload, traceNetworkRows) {
|
|
857
931
|
// Skip artifact processing if run creation failed
|
|
858
932
|
if (this.runCreationFailed) {
|
|
859
933
|
return;
|
|
@@ -861,19 +935,20 @@ class TestLensReporter {
|
|
|
861
935
|
const attachments = result.attachments;
|
|
862
936
|
for (const attachment of attachments) {
|
|
863
937
|
const artifactType = this.getArtifactType(attachment.name);
|
|
938
|
+
this.artifactsSeen += 1;
|
|
864
939
|
if (attachment.path) {
|
|
865
940
|
// Check if attachment should be processed based on config
|
|
866
941
|
const isVideo = artifactType === 'video' || attachment.contentType?.startsWith('video/');
|
|
867
942
|
const isScreenshot = artifactType === 'screenshot' || attachment.contentType?.startsWith('image/');
|
|
868
943
|
// Skip video if disabled in config
|
|
869
944
|
if (isVideo && !this.config.enableVideo) {
|
|
870
|
-
logger.debug(`[SKIP] Skipping video artifact ${attachment.name} - video capture disabled in config`);
|
|
945
|
+
this.logger.debug(`[SKIP] Skipping video artifact ${attachment.name} - video capture disabled in config`);
|
|
871
946
|
this.bumpArtifactStat('skipped', artifactType);
|
|
872
947
|
continue;
|
|
873
948
|
}
|
|
874
949
|
// Skip screenshot if disabled in config
|
|
875
950
|
if (isScreenshot && !this.config.enableScreenshot) {
|
|
876
|
-
logger.debug(`[SKIP] Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
|
|
951
|
+
this.logger.debug(`[SKIP] Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
|
|
877
952
|
this.bumpArtifactStat('skipped', artifactType);
|
|
878
953
|
continue;
|
|
879
954
|
}
|
|
@@ -940,16 +1015,12 @@ class TestLensReporter {
|
|
|
940
1015
|
}
|
|
941
1016
|
}
|
|
942
1017
|
const durationMs = Date.now() - traceStart;
|
|
943
|
-
if (networkRows.length > 0) {
|
|
944
|
-
|
|
945
|
-
}
|
|
946
|
-
else {
|
|
947
|
-
this.traceNetworkRows = [];
|
|
1018
|
+
if (networkRows.length > 0 && traceNetworkRows) {
|
|
1019
|
+
traceNetworkRows.push(...networkRows);
|
|
948
1020
|
}
|
|
949
1021
|
}
|
|
950
1022
|
catch (e) {
|
|
951
|
-
logger.warn('[TRACE] Could not read trace zip: ' + (e && e.message));
|
|
952
|
-
this.traceNetworkRows = [];
|
|
1023
|
+
this.logger.warn('[TRACE] Could not read trace zip: ' + (e && e.message));
|
|
953
1024
|
}
|
|
954
1025
|
}
|
|
955
1026
|
try {
|
|
@@ -987,14 +1058,14 @@ class TestLensReporter {
|
|
|
987
1058
|
const uploadPromise = Promise.resolve().then(async () => {
|
|
988
1059
|
try {
|
|
989
1060
|
if (!attachment.path) {
|
|
990
|
-
logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
|
|
1061
|
+
this.logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
|
|
991
1062
|
this.bumpArtifactStat('skipped', artifactType);
|
|
992
1063
|
return;
|
|
993
1064
|
}
|
|
994
1065
|
const s3Data = await this.uploadArtifactToS3(attachment.path, testId, fileName, testCaseDbId);
|
|
995
1066
|
// Skip if upload failed or file was too large
|
|
996
1067
|
if (!s3Data) {
|
|
997
|
-
logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
|
|
1068
|
+
this.logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
|
|
998
1069
|
this.bumpArtifactStat('failed', artifactType);
|
|
999
1070
|
return;
|
|
1000
1071
|
}
|
|
@@ -1021,10 +1092,15 @@ class TestLensReporter {
|
|
|
1021
1092
|
timestamp: new Date().toISOString(),
|
|
1022
1093
|
artifact: artifactData
|
|
1023
1094
|
});
|
|
1024
|
-
|
|
1095
|
+
// Log artifact upload success at info level
|
|
1096
|
+
this.logger.logArtifactStatus(testName, artifactType, 'uploaded');
|
|
1097
|
+
this.bumpArtifactStat('uploaded', artifactType);
|
|
1098
|
+
this.logger.debug(`[ARTIFACT] [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
|
|
1025
1099
|
}
|
|
1026
1100
|
catch (error) {
|
|
1027
|
-
|
|
1101
|
+
// Log artifact upload failure at error level
|
|
1102
|
+
this.logger.logArtifactStatus(testName, artifactType, 'failed');
|
|
1103
|
+
this.logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}: ${error.message}`);
|
|
1028
1104
|
this.bumpArtifactStat('failed', artifactType);
|
|
1029
1105
|
}
|
|
1030
1106
|
});
|
|
@@ -1037,7 +1113,7 @@ class TestLensReporter {
|
|
|
1037
1113
|
// They will be awaited in onEnd
|
|
1038
1114
|
}
|
|
1039
1115
|
catch (error) {
|
|
1040
|
-
logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}: ${error.message}`);
|
|
1116
|
+
this.logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}: ${error.message}`);
|
|
1041
1117
|
this.bumpArtifactStat('failed', artifactType);
|
|
1042
1118
|
}
|
|
1043
1119
|
}
|
|
@@ -1046,7 +1122,7 @@ class TestLensReporter {
|
|
|
1046
1122
|
}
|
|
1047
1123
|
}
|
|
1048
1124
|
}
|
|
1049
|
-
async sendSpecCodeBlocks(specPath, testName, errors, runId, test_id, testCaseId) {
|
|
1125
|
+
async sendSpecCodeBlocks(specPath, testName, errors, runId, test_id, testCaseId, traceNetworkRows = []) {
|
|
1050
1126
|
try {
|
|
1051
1127
|
// Extract code blocks using built-in parser
|
|
1052
1128
|
const testBlocks = this.extractTestBlocks(specPath);
|
|
@@ -1070,22 +1146,22 @@ class TestLensReporter {
|
|
|
1070
1146
|
filePath: path.relative(process.cwd(), specPath),
|
|
1071
1147
|
codeBlocks,
|
|
1072
1148
|
errors,
|
|
1073
|
-
traceNetworkRows:
|
|
1149
|
+
traceNetworkRows: traceNetworkRows,
|
|
1074
1150
|
testSuiteName: path.basename(specPath).replace(/\.(spec|test)\.(js|ts)$/, ''),
|
|
1075
1151
|
runId,
|
|
1076
1152
|
test_id,
|
|
1077
1153
|
testCaseId
|
|
1078
1154
|
});
|
|
1079
|
-
logger.debug(`Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
|
|
1155
|
+
this.logger.debug(`Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
|
|
1080
1156
|
}
|
|
1081
1157
|
catch (error) {
|
|
1082
1158
|
const errorData = error?.response?.data;
|
|
1083
1159
|
// Handle duplicate spec code blocks gracefully (when re-running tests)
|
|
1084
1160
|
if (errorData?.error && errorData.error.includes('duplicate key value violates unique constraint')) {
|
|
1085
|
-
logger.debug(`[INFO] Spec code blocks already exist for: ${path.basename(specPath)} (skipped)`);
|
|
1161
|
+
this.logger.debug(`[INFO] Spec code blocks already exist for: ${path.basename(specPath)} (skipped)`);
|
|
1086
1162
|
return;
|
|
1087
1163
|
}
|
|
1088
|
-
logger.error(`Failed to send spec code blocks: ${errorData || error?.message || 'Unknown error'}`);
|
|
1164
|
+
this.logger.error(`Failed to send spec code blocks: ${errorData || error?.message || 'Unknown error'}`);
|
|
1089
1165
|
}
|
|
1090
1166
|
}
|
|
1091
1167
|
extractTestBlocks(filePath) {
|
|
@@ -1185,7 +1261,7 @@ class TestLensReporter {
|
|
|
1185
1261
|
return blocks;
|
|
1186
1262
|
}
|
|
1187
1263
|
catch (error) {
|
|
1188
|
-
logger.error(`Failed to extract test blocks from ${filePath}: ${error.message}`);
|
|
1264
|
+
this.logger.error(`Failed to extract test blocks from ${filePath}: ${error.message}`);
|
|
1189
1265
|
return [];
|
|
1190
1266
|
}
|
|
1191
1267
|
}
|
|
@@ -1208,7 +1284,7 @@ class TestLensReporter {
|
|
|
1208
1284
|
}
|
|
1209
1285
|
catch (e) {
|
|
1210
1286
|
// Remote info is optional - handle gracefully
|
|
1211
|
-
logger.debug('[INFO] No git remote configured, skipping remote info');
|
|
1287
|
+
this.logger.debug('[INFO] No git remote configured, skipping remote info');
|
|
1212
1288
|
}
|
|
1213
1289
|
const isDirty = (0, child_process_1.execSync)('git status --porcelain', { encoding: 'utf-8' }).trim().length > 0;
|
|
1214
1290
|
return {
|
|
@@ -1286,7 +1362,7 @@ class TestLensReporter {
|
|
|
1286
1362
|
// Check file size first
|
|
1287
1363
|
const fileSize = this.getFileSize(filePath);
|
|
1288
1364
|
const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
|
|
1289
|
-
logger.debug(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
|
|
1365
|
+
this.logger.debug(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
|
|
1290
1366
|
const baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
|
|
1291
1367
|
// Step 1: Request pre-signed URL from server
|
|
1292
1368
|
const presignedUrlEndpoint = `${baseUrl}/api/v1/artifacts/public/presigned-url`;
|
|
@@ -1310,7 +1386,7 @@ class TestLensReporter {
|
|
|
1310
1386
|
}
|
|
1311
1387
|
const { uploadUrl, s3Key, metadata } = presignedResponse.data;
|
|
1312
1388
|
// Step 2: Upload directly to S3 using presigned URL
|
|
1313
|
-
logger.debug(`[UPLOAD] [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
|
|
1389
|
+
this.logger.debug(`[UPLOAD] [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
|
|
1314
1390
|
const fileBuffer = fs.readFileSync(filePath);
|
|
1315
1391
|
// IMPORTANT: When using presigned URLs, we MUST include exactly the headers that were signed
|
|
1316
1392
|
// The backend signs with ServerSideEncryption:'AES256', so we must send that header
|
|
@@ -1328,7 +1404,7 @@ class TestLensReporter {
|
|
|
1328
1404
|
if (uploadResponse.status !== 200) {
|
|
1329
1405
|
throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
|
|
1330
1406
|
}
|
|
1331
|
-
logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
|
|
1407
|
+
this.logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
|
|
1332
1408
|
// Step 3: Confirm upload with server to save metadata
|
|
1333
1409
|
const confirmEndpoint = `${baseUrl}/api/v1/artifacts/public/confirm-upload`;
|
|
1334
1410
|
const confirmBody = {
|
|
@@ -1349,7 +1425,7 @@ class TestLensReporter {
|
|
|
1349
1425
|
});
|
|
1350
1426
|
if (confirmResponse.status === 201 && confirmResponse.data.success) {
|
|
1351
1427
|
const artifact = confirmResponse.data.artifact;
|
|
1352
|
-
logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
|
|
1428
|
+
this.logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
|
|
1353
1429
|
return {
|
|
1354
1430
|
key: s3Key,
|
|
1355
1431
|
url: artifact.s3Url,
|
|
@@ -1368,29 +1444,29 @@ class TestLensReporter {
|
|
|
1368
1444
|
const errorData = error?.response?.data;
|
|
1369
1445
|
if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
|
|
1370
1446
|
errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
|
|
1371
|
-
logger.error('\n' + '='.repeat(80));
|
|
1447
|
+
this.logger.error('\n' + '='.repeat(80));
|
|
1372
1448
|
if (errorData?.error === 'test_cases_limit_reached') {
|
|
1373
|
-
logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
|
|
1449
|
+
this.logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
|
|
1374
1450
|
}
|
|
1375
1451
|
else if (errorData?.error === 'test_runs_limit_reached') {
|
|
1376
|
-
logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
|
|
1452
|
+
this.logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
|
|
1377
1453
|
}
|
|
1378
1454
|
else {
|
|
1379
|
-
logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
|
|
1455
|
+
this.logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
|
|
1380
1456
|
}
|
|
1381
|
-
logger.error('='.repeat(80));
|
|
1382
|
-
logger.error('');
|
|
1383
|
-
logger.error(errorData?.message || 'Your trial period has expired.');
|
|
1384
|
-
logger.error('');
|
|
1385
|
-
logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
|
|
1386
|
-
logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
|
|
1387
|
-
logger.error('');
|
|
1457
|
+
this.logger.error('='.repeat(80));
|
|
1458
|
+
this.logger.error('');
|
|
1459
|
+
this.logger.error(errorData?.message || 'Your trial period has expired.');
|
|
1460
|
+
this.logger.error('');
|
|
1461
|
+
this.logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
|
|
1462
|
+
this.logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
|
|
1463
|
+
this.logger.error('');
|
|
1388
1464
|
if (errorData?.trial_end_date) {
|
|
1389
|
-
logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
|
|
1390
|
-
logger.error('');
|
|
1465
|
+
this.logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
|
|
1466
|
+
this.logger.error('');
|
|
1391
1467
|
}
|
|
1392
|
-
logger.error('='.repeat(80));
|
|
1393
|
-
logger.error('');
|
|
1468
|
+
this.logger.error('='.repeat(80));
|
|
1469
|
+
this.logger.error('');
|
|
1394
1470
|
return null;
|
|
1395
1471
|
}
|
|
1396
1472
|
}
|
|
@@ -1408,9 +1484,9 @@ class TestLensReporter {
|
|
|
1408
1484
|
else if (error.response?.status === 403) {
|
|
1409
1485
|
errorMsg = `Access denied (403) - presigned URL may have expired`;
|
|
1410
1486
|
}
|
|
1411
|
-
logger.error(`[ERROR] Failed to upload ${fileName} to S3: ${errorMsg}`);
|
|
1487
|
+
this.logger.error(`[ERROR] Failed to upload ${fileName} to S3: ${errorMsg}`);
|
|
1412
1488
|
if (error.response?.data) {
|
|
1413
|
-
logger.error({ errorDetails: error.response.data }, 'Error details');
|
|
1489
|
+
this.logger.error({ errorDetails: error.response.data }, 'Error details');
|
|
1414
1490
|
}
|
|
1415
1491
|
// Don't throw, just return null to continue with other artifacts
|
|
1416
1492
|
return null;
|
|
@@ -1455,7 +1531,7 @@ class TestLensReporter {
|
|
|
1455
1531
|
return stats.size;
|
|
1456
1532
|
}
|
|
1457
1533
|
catch (error) {
|
|
1458
|
-
logger.warn(`Could not get file size for ${filePath}: ${error.message}`);
|
|
1534
|
+
this.logger.warn(`Could not get file size for ${filePath}: ${error.message}`);
|
|
1459
1535
|
return 0;
|
|
1460
1536
|
}
|
|
1461
1537
|
}
|