@alternative-path/testlens-playwright-reporter 0.4.5 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +3 -0
- package/index.js +130 -19
- package/index.ts +135 -20
- package/package.json +1 -1
- package/postinstall.js +49 -44
package/index.d.ts
CHANGED
|
@@ -161,6 +161,8 @@ export declare class TestLensReporter implements Reporter {
|
|
|
161
161
|
private runCreationFailed;
|
|
162
162
|
private cliArgs;
|
|
163
163
|
private pendingUploads;
|
|
164
|
+
private artifactStats;
|
|
165
|
+
private artifactsSeen;
|
|
164
166
|
/**
|
|
165
167
|
* Parse custom metadata from environment variables
|
|
166
168
|
* Checks for common metadata environment variables
|
|
@@ -184,6 +186,7 @@ export declare class TestLensReporter implements Reporter {
|
|
|
184
186
|
private extractTestBlocks;
|
|
185
187
|
private collectGitInfo;
|
|
186
188
|
private getArtifactType;
|
|
189
|
+
private bumpArtifactStat;
|
|
187
190
|
private extractTags;
|
|
188
191
|
private getTestId;
|
|
189
192
|
private uploadArtifactToS3;
|
package/index.js
CHANGED
|
@@ -10,9 +10,64 @@ 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 isVerboseLogs = process.env.TESTLENS_LOG_VERBOSE === '1' ||
|
|
14
|
+
process.env.TESTLENS_LOG_VERBOSE === 'true';
|
|
15
|
+
const noisyInfoPrefixes = [
|
|
16
|
+
'[OK]',
|
|
17
|
+
'[UPLOAD]',
|
|
18
|
+
'[ARTIFACT]',
|
|
19
|
+
'[SKIP]',
|
|
20
|
+
'[TestLens] onTestEnd called',
|
|
21
|
+
'[TestLens] Sending testEnd'
|
|
22
|
+
];
|
|
23
|
+
const noisyInfoSubstrings = [
|
|
24
|
+
'Sent runStart event to TestLens',
|
|
25
|
+
'Sent specStart event to TestLens',
|
|
26
|
+
'Sent testStart event to TestLens',
|
|
27
|
+
'Sent testEnd event to TestLens',
|
|
28
|
+
'Sent runEnd event to TestLens',
|
|
29
|
+
'Sent artifact event to TestLens',
|
|
30
|
+
'S3 upload completed',
|
|
31
|
+
'Upload confirmed',
|
|
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
|
+
};
|
|
13
47
|
// Create pino logger instance
|
|
14
48
|
const logger = (0, pino_1.default)({
|
|
15
|
-
level: process.env.LOG_LEVEL || 'info'
|
|
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;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return method.apply(this, args);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
16
71
|
});
|
|
17
72
|
// Lazy-load mime module to support ESM
|
|
18
73
|
let mimeModule = null;
|
|
@@ -66,6 +121,12 @@ class TestLensReporter {
|
|
|
66
121
|
this.runCreationFailed = false; // Track if run creation failed due to limits
|
|
67
122
|
this.cliArgs = {}; // Store CLI args separately
|
|
68
123
|
this.pendingUploads = new Set(); // Track pending artifact uploads
|
|
124
|
+
this.artifactStats = {
|
|
125
|
+
uploaded: { screenshot: 0, video: 0, trace: 0, attachment: 0 },
|
|
126
|
+
skipped: { screenshot: 0, video: 0, trace: 0, attachment: 0 },
|
|
127
|
+
failed: { screenshot: 0, video: 0, trace: 0, attachment: 0 }
|
|
128
|
+
};
|
|
129
|
+
this.artifactsSeen = 0;
|
|
69
130
|
// Parse custom CLI arguments
|
|
70
131
|
const customArgs = TestLensReporter.parseCustomArgs();
|
|
71
132
|
this.cliArgs = customArgs; // Store CLI args separately for later use
|
|
@@ -235,19 +296,20 @@ class TestLensReporter {
|
|
|
235
296
|
return status;
|
|
236
297
|
}
|
|
237
298
|
async onBegin(config, suite) {
|
|
299
|
+
logger.info(`[REPORTER] source=${__filename} enableArtifacts=${this.config.enableArtifacts}`);
|
|
238
300
|
// Show Build Name if provided, otherwise show Run ID
|
|
239
301
|
if (this.runMetadata.testlensBuildName) {
|
|
240
|
-
logger.info(
|
|
302
|
+
logger.info(`TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
|
|
241
303
|
logger.info(` Run ID: ${this.runId}`);
|
|
242
304
|
}
|
|
243
305
|
else {
|
|
244
|
-
logger.info(
|
|
306
|
+
logger.info(`TestLens Reporter starting - Run ID: ${this.runId}`);
|
|
245
307
|
}
|
|
246
308
|
// Collect Git information if enabled
|
|
247
309
|
if (this.config.enableGitInfo) {
|
|
248
310
|
this.runMetadata.gitInfo = await this.collectGitInfo();
|
|
249
311
|
if (this.runMetadata.gitInfo) {
|
|
250
|
-
logger.info(
|
|
312
|
+
logger.info(`Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
|
|
251
313
|
}
|
|
252
314
|
else {
|
|
253
315
|
logger.warn(`[WARN] Git info collection returned null - not in a git repository or git not available`);
|
|
@@ -273,7 +335,7 @@ class TestLensReporter {
|
|
|
273
335
|
}
|
|
274
336
|
async onTestBegin(test, result) {
|
|
275
337
|
// Log which test is starting
|
|
276
|
-
logger.info(
|
|
338
|
+
logger.info(`[TEST] Running test: ${test.title}`);
|
|
277
339
|
const specPath = test.location.file;
|
|
278
340
|
const specKey = `${specPath}-${test.parent.title}`;
|
|
279
341
|
// Create or update spec data
|
|
@@ -346,6 +408,19 @@ class TestLensReporter {
|
|
|
346
408
|
async onTestEnd(test, result) {
|
|
347
409
|
const testId = this.getTestId(test);
|
|
348
410
|
let testData = this.testMap.get(testId);
|
|
411
|
+
logger.info(`[ARTIFACTS] attachments=${result.attachments?.length ?? 0}`);
|
|
412
|
+
if (result.attachments && result.attachments.length > 0) {
|
|
413
|
+
for (const attachment of result.attachments) {
|
|
414
|
+
const artifactType = this.getArtifactType(attachment.name);
|
|
415
|
+
this.artifactsSeen += 1;
|
|
416
|
+
if (attachment.path) {
|
|
417
|
+
this.bumpArtifactStat('uploaded', artifactType);
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
this.bumpArtifactStat('skipped', artifactType);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
349
424
|
logger.info(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
|
|
350
425
|
// For skipped tests, onTestBegin might not be called, so we need to create the test data here
|
|
351
426
|
if (!testData) {
|
|
@@ -507,6 +582,13 @@ class TestLensReporter {
|
|
|
507
582
|
// Pass test case DB ID if available for faster lookups
|
|
508
583
|
await this.processArtifacts(testId, result, testEndResponse?.testCaseId);
|
|
509
584
|
}
|
|
585
|
+
else if (result.attachments && result.attachments.length > 0) {
|
|
586
|
+
for (const attachment of result.attachments) {
|
|
587
|
+
const artifactType = this.getArtifactType(attachment.name);
|
|
588
|
+
this.artifactsSeen += 1;
|
|
589
|
+
this.bumpArtifactStat('skipped', artifactType);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
510
592
|
}
|
|
511
593
|
// Update spec status
|
|
512
594
|
const specPath = test.location.file;
|
|
@@ -577,15 +659,29 @@ class TestLensReporter {
|
|
|
577
659
|
this.runMetadata.duration = Date.now() - new Date(this.runMetadata.startTime).getTime();
|
|
578
660
|
// Wait for all pending artifact uploads to complete before sending runEnd
|
|
579
661
|
if (this.pendingUploads.size > 0) {
|
|
580
|
-
logger.
|
|
662
|
+
logger.debug(`Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
|
|
581
663
|
try {
|
|
582
664
|
await Promise.all(Array.from(this.pendingUploads));
|
|
583
|
-
logger.
|
|
665
|
+
logger.debug(`[OK] All artifact uploads completed`);
|
|
584
666
|
}
|
|
585
667
|
catch (error) {
|
|
586
668
|
logger.error(`[WARN] Some artifact uploads failed, continuing with runEnd`);
|
|
587
669
|
}
|
|
588
670
|
}
|
|
671
|
+
const uploaded = this.artifactStats.uploaded;
|
|
672
|
+
const skipped = this.artifactStats.skipped;
|
|
673
|
+
const failed = this.artifactStats.failed;
|
|
674
|
+
// Always show summary so users can confirm artifact settings
|
|
675
|
+
const summary = `[ARTIFACTS] seen=${this.artifactsSeen} | ` +
|
|
676
|
+
`uploaded screenshot=${uploaded.screenshot}, video=${uploaded.video}, trace=${uploaded.trace}, attachment=${uploaded.attachment} | ` +
|
|
677
|
+
`skipped screenshot=${skipped.screenshot}, video=${skipped.video}, trace=${skipped.trace}, attachment=${skipped.attachment} | ` +
|
|
678
|
+
`failed screenshot=${failed.screenshot}, video=${failed.video}, trace=${failed.trace}, attachment=${failed.attachment}`;
|
|
679
|
+
if (logger.levelVal > 30) {
|
|
680
|
+
console.log(summary);
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
logger.info(summary);
|
|
684
|
+
}
|
|
589
685
|
// Calculate final stats
|
|
590
686
|
const totalTests = Array.from(this.testMap.values()).length;
|
|
591
687
|
const passedTests = Array.from(this.testMap.values()).filter(t => t.status === 'passed').length;
|
|
@@ -634,7 +730,7 @@ class TestLensReporter {
|
|
|
634
730
|
}
|
|
635
731
|
});
|
|
636
732
|
if (this.config.enableRealTimeStream) {
|
|
637
|
-
logger.
|
|
733
|
+
logger.debug(`[OK] Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
|
|
638
734
|
}
|
|
639
735
|
// Return response data for caller to use
|
|
640
736
|
return response.data;
|
|
@@ -726,19 +822,21 @@ class TestLensReporter {
|
|
|
726
822
|
}
|
|
727
823
|
const attachments = result.attachments;
|
|
728
824
|
for (const attachment of attachments) {
|
|
825
|
+
const artifactType = this.getArtifactType(attachment.name);
|
|
729
826
|
if (attachment.path) {
|
|
730
827
|
// Check if attachment should be processed based on config
|
|
731
|
-
const artifactType = this.getArtifactType(attachment.name);
|
|
732
828
|
const isVideo = artifactType === 'video' || attachment.contentType?.startsWith('video/');
|
|
733
829
|
const isScreenshot = artifactType === 'screenshot' || attachment.contentType?.startsWith('image/');
|
|
734
830
|
// Skip video if disabled in config
|
|
735
831
|
if (isVideo && !this.config.enableVideo) {
|
|
736
|
-
logger.
|
|
832
|
+
logger.debug(`[SKIP] Skipping video artifact ${attachment.name} - video capture disabled in config`);
|
|
833
|
+
this.bumpArtifactStat('skipped', artifactType);
|
|
737
834
|
continue;
|
|
738
835
|
}
|
|
739
836
|
// Skip screenshot if disabled in config
|
|
740
837
|
if (isScreenshot && !this.config.enableScreenshot) {
|
|
741
|
-
logger.
|
|
838
|
+
logger.debug(`[SKIP] Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
|
|
839
|
+
this.bumpArtifactStat('skipped', artifactType);
|
|
742
840
|
continue;
|
|
743
841
|
}
|
|
744
842
|
try {
|
|
@@ -776,13 +874,15 @@ class TestLensReporter {
|
|
|
776
874
|
const uploadPromise = Promise.resolve().then(async () => {
|
|
777
875
|
try {
|
|
778
876
|
if (!attachment.path) {
|
|
779
|
-
logger.
|
|
877
|
+
logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
|
|
878
|
+
this.bumpArtifactStat('skipped', artifactType);
|
|
780
879
|
return;
|
|
781
880
|
}
|
|
782
881
|
const s3Data = await this.uploadArtifactToS3(attachment.path, testId, fileName, testCaseDbId);
|
|
783
882
|
// Skip if upload failed or file was too large
|
|
784
883
|
if (!s3Data) {
|
|
785
|
-
logger.
|
|
884
|
+
logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
|
|
885
|
+
this.bumpArtifactStat('failed', artifactType);
|
|
786
886
|
return;
|
|
787
887
|
}
|
|
788
888
|
const artifactData = {
|
|
@@ -803,10 +903,11 @@ class TestLensReporter {
|
|
|
803
903
|
timestamp: new Date().toISOString(),
|
|
804
904
|
artifact: artifactData
|
|
805
905
|
});
|
|
806
|
-
logger.
|
|
906
|
+
logger.debug(`[ARTIFACT] [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
|
|
807
907
|
}
|
|
808
908
|
catch (error) {
|
|
809
909
|
logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}: ${error.message}`);
|
|
910
|
+
this.bumpArtifactStat('failed', artifactType);
|
|
810
911
|
}
|
|
811
912
|
});
|
|
812
913
|
// Track this upload and ensure cleanup on completion
|
|
@@ -819,8 +920,12 @@ class TestLensReporter {
|
|
|
819
920
|
}
|
|
820
921
|
catch (error) {
|
|
821
922
|
logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}: ${error.message}`);
|
|
923
|
+
this.bumpArtifactStat('failed', artifactType);
|
|
822
924
|
}
|
|
823
925
|
}
|
|
926
|
+
else {
|
|
927
|
+
this.bumpArtifactStat('skipped', artifactType);
|
|
928
|
+
}
|
|
824
929
|
}
|
|
825
930
|
}
|
|
826
931
|
async sendSpecCodeBlocks(specPath) {
|
|
@@ -848,7 +953,7 @@ class TestLensReporter {
|
|
|
848
953
|
codeBlocks,
|
|
849
954
|
testSuiteName: path.basename(specPath).replace(/\.(spec|test)\.(js|ts)$/, '')
|
|
850
955
|
});
|
|
851
|
-
logger.info(
|
|
956
|
+
logger.info(`Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
|
|
852
957
|
}
|
|
853
958
|
catch (error) {
|
|
854
959
|
const errorData = error?.response?.data;
|
|
@@ -968,6 +1073,12 @@ class TestLensReporter {
|
|
|
968
1073
|
return 'trace';
|
|
969
1074
|
return 'attachment';
|
|
970
1075
|
}
|
|
1076
|
+
bumpArtifactStat(stat, type) {
|
|
1077
|
+
const bucket = this.artifactStats[stat];
|
|
1078
|
+
if (bucket[type] !== undefined) {
|
|
1079
|
+
bucket[type] += 1;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
971
1082
|
extractTags(test) {
|
|
972
1083
|
const tags = [];
|
|
973
1084
|
// Playwright stores tags in the _tags property
|
|
@@ -1011,7 +1122,7 @@ class TestLensReporter {
|
|
|
1011
1122
|
// Check file size first
|
|
1012
1123
|
const fileSize = this.getFileSize(filePath);
|
|
1013
1124
|
const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
|
|
1014
|
-
logger.
|
|
1125
|
+
logger.debug(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
|
|
1015
1126
|
const baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
|
|
1016
1127
|
// Step 1: Request pre-signed URL from server
|
|
1017
1128
|
const presignedUrlEndpoint = `${baseUrl}/api/v1/artifacts/public/presigned-url`;
|
|
@@ -1036,7 +1147,7 @@ class TestLensReporter {
|
|
|
1036
1147
|
}
|
|
1037
1148
|
const { uploadUrl, s3Key, metadata } = presignedResponse.data;
|
|
1038
1149
|
// Step 2: Upload directly to S3 using presigned URL
|
|
1039
|
-
logger.
|
|
1150
|
+
logger.debug(`[UPLOAD] [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
|
|
1040
1151
|
const fileBuffer = fs.readFileSync(filePath);
|
|
1041
1152
|
// IMPORTANT: When using presigned URLs, we MUST include exactly the headers that were signed
|
|
1042
1153
|
// The backend signs with ServerSideEncryption:'AES256', so we must send that header
|
|
@@ -1054,7 +1165,7 @@ class TestLensReporter {
|
|
|
1054
1165
|
if (uploadResponse.status !== 200) {
|
|
1055
1166
|
throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
|
|
1056
1167
|
}
|
|
1057
|
-
logger.
|
|
1168
|
+
logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
|
|
1058
1169
|
// Step 3: Confirm upload with server to save metadata
|
|
1059
1170
|
const confirmEndpoint = `${baseUrl}/api/v1/artifacts/public/confirm-upload`;
|
|
1060
1171
|
const confirmBody = {
|
|
@@ -1076,7 +1187,7 @@ class TestLensReporter {
|
|
|
1076
1187
|
});
|
|
1077
1188
|
if (confirmResponse.status === 201 && confirmResponse.data.success) {
|
|
1078
1189
|
const artifact = confirmResponse.data.artifact;
|
|
1079
|
-
logger.
|
|
1190
|
+
logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
|
|
1080
1191
|
return {
|
|
1081
1192
|
key: s3Key,
|
|
1082
1193
|
url: artifact.s3Url,
|
package/index.ts
CHANGED
|
@@ -8,9 +8,68 @@ 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 isVerboseLogs =
|
|
12
|
+
process.env.TESTLENS_LOG_VERBOSE === '1' ||
|
|
13
|
+
process.env.TESTLENS_LOG_VERBOSE === 'true';
|
|
14
|
+
const noisyInfoPrefixes = [
|
|
15
|
+
'[OK]',
|
|
16
|
+
'[UPLOAD]',
|
|
17
|
+
'[ARTIFACT]',
|
|
18
|
+
'[SKIP]',
|
|
19
|
+
'[TestLens] onTestEnd called',
|
|
20
|
+
'[TestLens] Sending testEnd'
|
|
21
|
+
];
|
|
22
|
+
const noisyInfoSubstrings = [
|
|
23
|
+
'Sent runStart event to TestLens',
|
|
24
|
+
'Sent specStart event to TestLens',
|
|
25
|
+
'Sent testStart event to TestLens',
|
|
26
|
+
'Sent testEnd event to TestLens',
|
|
27
|
+
'Sent runEnd event to TestLens',
|
|
28
|
+
'Sent artifact event to TestLens',
|
|
29
|
+
'S3 upload completed',
|
|
30
|
+
'Upload confirmed',
|
|
31
|
+
'Uploading ',
|
|
32
|
+
'Processed artifact',
|
|
33
|
+
'Spec code blocks',
|
|
34
|
+
'\n[METADATA]',
|
|
35
|
+
'Custom Metadata Detected',
|
|
36
|
+
'Using API key from environment variable'
|
|
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
|
+
|
|
11
48
|
// Create pino logger instance
|
|
12
49
|
const logger = pino({
|
|
13
|
-
level: process.env.LOG_LEVEL || 'info'
|
|
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;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return method.apply(this, args as any);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
14
73
|
});
|
|
15
74
|
|
|
16
75
|
// Lazy-load mime module to support ESM
|
|
@@ -191,6 +250,12 @@ export class TestLensReporter implements Reporter {
|
|
|
191
250
|
private runCreationFailed: boolean = false; // Track if run creation failed due to limits
|
|
192
251
|
private cliArgs: Record<string, any> = {}; // Store CLI args separately
|
|
193
252
|
private pendingUploads: Set<Promise<any>> = new Set(); // Track pending artifact uploads
|
|
253
|
+
private artifactStats = {
|
|
254
|
+
uploaded: { screenshot: 0, video: 0, trace: 0, attachment: 0 },
|
|
255
|
+
skipped: { screenshot: 0, video: 0, trace: 0, attachment: 0 },
|
|
256
|
+
failed: { screenshot: 0, video: 0, trace: 0, attachment: 0 }
|
|
257
|
+
};
|
|
258
|
+
private artifactsSeen: number = 0;
|
|
194
259
|
|
|
195
260
|
/**
|
|
196
261
|
* Parse custom metadata from environment variables
|
|
@@ -423,19 +488,20 @@ export class TestLensReporter implements Reporter {
|
|
|
423
488
|
}
|
|
424
489
|
|
|
425
490
|
async onBegin(config: FullConfig, suite: Suite): Promise<void> {
|
|
491
|
+
logger.info(`[REPORTER] source=${__filename} enableArtifacts=${this.config.enableArtifacts}`);
|
|
426
492
|
// Show Build Name if provided, otherwise show Run ID
|
|
427
493
|
if (this.runMetadata.testlensBuildName) {
|
|
428
|
-
logger.info(
|
|
494
|
+
logger.info(`TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
|
|
429
495
|
logger.info(` Run ID: ${this.runId}`);
|
|
430
496
|
} else {
|
|
431
|
-
logger.info(
|
|
497
|
+
logger.info(`TestLens Reporter starting - Run ID: ${this.runId}`);
|
|
432
498
|
}
|
|
433
499
|
|
|
434
500
|
// Collect Git information if enabled
|
|
435
501
|
if (this.config.enableGitInfo) {
|
|
436
502
|
this.runMetadata.gitInfo = await this.collectGitInfo();
|
|
437
503
|
if (this.runMetadata.gitInfo) {
|
|
438
|
-
logger.info(
|
|
504
|
+
logger.info(`Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
|
|
439
505
|
} else {
|
|
440
506
|
logger.warn(`[WARN] Git info collection returned null - not in a git repository or git not available`);
|
|
441
507
|
}
|
|
@@ -462,7 +528,7 @@ export class TestLensReporter implements Reporter {
|
|
|
462
528
|
|
|
463
529
|
async onTestBegin(test: TestCase, result: TestResult): Promise<void> {
|
|
464
530
|
// Log which test is starting
|
|
465
|
-
logger.info(
|
|
531
|
+
logger.info(`[TEST] Running test: ${test.title}`);
|
|
466
532
|
|
|
467
533
|
const specPath = test.location.file;
|
|
468
534
|
const specKey = `${specPath}-${test.parent.title}`;
|
|
@@ -543,6 +609,20 @@ export class TestLensReporter implements Reporter {
|
|
|
543
609
|
const testId = this.getTestId(test);
|
|
544
610
|
let testData = this.testMap.get(testId);
|
|
545
611
|
|
|
612
|
+
logger.info(`[ARTIFACTS] attachments=${result.attachments?.length ?? 0}`);
|
|
613
|
+
|
|
614
|
+
if (result.attachments && result.attachments.length > 0) {
|
|
615
|
+
for (const attachment of result.attachments) {
|
|
616
|
+
const artifactType = this.getArtifactType(attachment.name);
|
|
617
|
+
this.artifactsSeen += 1;
|
|
618
|
+
if (attachment.path) {
|
|
619
|
+
this.bumpArtifactStat('uploaded', artifactType);
|
|
620
|
+
} else {
|
|
621
|
+
this.bumpArtifactStat('skipped', artifactType);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
546
626
|
logger.info(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
|
|
547
627
|
|
|
548
628
|
// For skipped tests, onTestBegin might not be called, so we need to create the test data here
|
|
@@ -726,6 +806,12 @@ export class TestLensReporter implements Reporter {
|
|
|
726
806
|
if (this.config.enableArtifacts) {
|
|
727
807
|
// Pass test case DB ID if available for faster lookups
|
|
728
808
|
await this.processArtifacts(testId, result, testEndResponse?.testCaseId);
|
|
809
|
+
} else if (result.attachments && result.attachments.length > 0) {
|
|
810
|
+
for (const attachment of result.attachments) {
|
|
811
|
+
const artifactType = this.getArtifactType(attachment.name);
|
|
812
|
+
this.artifactsSeen += 1;
|
|
813
|
+
this.bumpArtifactStat('skipped', artifactType);
|
|
814
|
+
}
|
|
729
815
|
}
|
|
730
816
|
}
|
|
731
817
|
|
|
@@ -803,15 +889,30 @@ export class TestLensReporter implements Reporter {
|
|
|
803
889
|
|
|
804
890
|
// Wait for all pending artifact uploads to complete before sending runEnd
|
|
805
891
|
if (this.pendingUploads.size > 0) {
|
|
806
|
-
logger.
|
|
892
|
+
logger.debug(`Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
|
|
807
893
|
try {
|
|
808
894
|
await Promise.all(Array.from(this.pendingUploads));
|
|
809
|
-
logger.
|
|
895
|
+
logger.debug(`[OK] All artifact uploads completed`);
|
|
810
896
|
} catch (error) {
|
|
811
897
|
logger.error(`[WARN] Some artifact uploads failed, continuing with runEnd`);
|
|
812
898
|
}
|
|
813
899
|
}
|
|
814
900
|
|
|
901
|
+
const uploaded = this.artifactStats.uploaded;
|
|
902
|
+
const skipped = this.artifactStats.skipped;
|
|
903
|
+
const failed = this.artifactStats.failed;
|
|
904
|
+
// Always show summary so users can confirm artifact settings
|
|
905
|
+
const summary =
|
|
906
|
+
`[ARTIFACTS] seen=${this.artifactsSeen} | ` +
|
|
907
|
+
`uploaded screenshot=${uploaded.screenshot}, video=${uploaded.video}, trace=${uploaded.trace}, attachment=${uploaded.attachment} | ` +
|
|
908
|
+
`skipped screenshot=${skipped.screenshot}, video=${skipped.video}, trace=${skipped.trace}, attachment=${skipped.attachment} | ` +
|
|
909
|
+
`failed screenshot=${failed.screenshot}, video=${failed.video}, trace=${failed.trace}, attachment=${failed.attachment}`;
|
|
910
|
+
if (logger.levelVal > 30) {
|
|
911
|
+
console.log(summary);
|
|
912
|
+
} else {
|
|
913
|
+
logger.info(summary);
|
|
914
|
+
}
|
|
915
|
+
|
|
815
916
|
// Calculate final stats
|
|
816
917
|
const totalTests = Array.from(this.testMap.values()).length;
|
|
817
918
|
const passedTests = Array.from(this.testMap.values()).filter(t => t.status === 'passed').length;
|
|
@@ -864,7 +965,7 @@ export class TestLensReporter implements Reporter {
|
|
|
864
965
|
}
|
|
865
966
|
});
|
|
866
967
|
if (this.config.enableRealTimeStream) {
|
|
867
|
-
logger.
|
|
968
|
+
logger.debug(`[OK] Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
|
|
868
969
|
}
|
|
869
970
|
// Return response data for caller to use
|
|
870
971
|
return response.data;
|
|
@@ -958,21 +1059,23 @@ export class TestLensReporter implements Reporter {
|
|
|
958
1059
|
const attachments = result.attachments;
|
|
959
1060
|
|
|
960
1061
|
for (const attachment of attachments) {
|
|
1062
|
+
const artifactType = this.getArtifactType(attachment.name);
|
|
961
1063
|
if (attachment.path) {
|
|
962
1064
|
// Check if attachment should be processed based on config
|
|
963
|
-
const artifactType = this.getArtifactType(attachment.name);
|
|
964
1065
|
const isVideo = artifactType === 'video' || attachment.contentType?.startsWith('video/');
|
|
965
1066
|
const isScreenshot = artifactType === 'screenshot' || attachment.contentType?.startsWith('image/');
|
|
966
1067
|
|
|
967
1068
|
// Skip video if disabled in config
|
|
968
1069
|
if (isVideo && !this.config.enableVideo) {
|
|
969
|
-
logger.
|
|
1070
|
+
logger.debug(`[SKIP] Skipping video artifact ${attachment.name} - video capture disabled in config`);
|
|
1071
|
+
this.bumpArtifactStat('skipped', artifactType);
|
|
970
1072
|
continue;
|
|
971
1073
|
}
|
|
972
1074
|
|
|
973
1075
|
// Skip screenshot if disabled in config
|
|
974
1076
|
if (isScreenshot && !this.config.enableScreenshot) {
|
|
975
|
-
logger.
|
|
1077
|
+
logger.debug(`[SKIP] Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
|
|
1078
|
+
this.bumpArtifactStat('skipped', artifactType);
|
|
976
1079
|
continue;
|
|
977
1080
|
}
|
|
978
1081
|
|
|
@@ -1012,7 +1115,8 @@ export class TestLensReporter implements Reporter {
|
|
|
1012
1115
|
const uploadPromise = Promise.resolve().then(async () => {
|
|
1013
1116
|
try {
|
|
1014
1117
|
if (!attachment.path) {
|
|
1015
|
-
logger.
|
|
1118
|
+
logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
|
|
1119
|
+
this.bumpArtifactStat('skipped', artifactType);
|
|
1016
1120
|
return;
|
|
1017
1121
|
}
|
|
1018
1122
|
|
|
@@ -1020,7 +1124,8 @@ export class TestLensReporter implements Reporter {
|
|
|
1020
1124
|
|
|
1021
1125
|
// Skip if upload failed or file was too large
|
|
1022
1126
|
if (!s3Data) {
|
|
1023
|
-
logger.
|
|
1127
|
+
logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
|
|
1128
|
+
this.bumpArtifactStat('failed', artifactType);
|
|
1024
1129
|
return;
|
|
1025
1130
|
}
|
|
1026
1131
|
|
|
@@ -1044,9 +1149,10 @@ export class TestLensReporter implements Reporter {
|
|
|
1044
1149
|
artifact: artifactData
|
|
1045
1150
|
});
|
|
1046
1151
|
|
|
1047
|
-
logger.
|
|
1152
|
+
logger.debug(`[ARTIFACT] [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
|
|
1048
1153
|
} catch (error) {
|
|
1049
1154
|
logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}: ${(error as Error).message}`);
|
|
1155
|
+
this.bumpArtifactStat('failed', artifactType);
|
|
1050
1156
|
}
|
|
1051
1157
|
});
|
|
1052
1158
|
|
|
@@ -1060,7 +1166,10 @@ export class TestLensReporter implements Reporter {
|
|
|
1060
1166
|
// They will be awaited in onEnd
|
|
1061
1167
|
} catch (error) {
|
|
1062
1168
|
logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}: ${(error as Error).message}`);
|
|
1169
|
+
this.bumpArtifactStat('failed', artifactType);
|
|
1063
1170
|
}
|
|
1171
|
+
} else {
|
|
1172
|
+
this.bumpArtifactStat('skipped', artifactType);
|
|
1064
1173
|
}
|
|
1065
1174
|
}
|
|
1066
1175
|
}
|
|
@@ -1094,7 +1203,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1094
1203
|
testSuiteName: path.basename(specPath).replace(/\.(spec|test)\.(js|ts)$/, '')
|
|
1095
1204
|
});
|
|
1096
1205
|
|
|
1097
|
-
logger.info(
|
|
1206
|
+
logger.info(`Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
|
|
1098
1207
|
} catch (error: any) {
|
|
1099
1208
|
const errorData = error?.response?.data;
|
|
1100
1209
|
|
|
@@ -1223,6 +1332,13 @@ export class TestLensReporter implements Reporter {
|
|
|
1223
1332
|
return 'attachment';
|
|
1224
1333
|
}
|
|
1225
1334
|
|
|
1335
|
+
private bumpArtifactStat(stat: 'uploaded' | 'skipped' | 'failed', type: string): void {
|
|
1336
|
+
const bucket = this.artifactStats[stat] as Record<string, number>;
|
|
1337
|
+
if (bucket[type] !== undefined) {
|
|
1338
|
+
bucket[type] += 1;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1226
1342
|
private extractTags(test: TestCase): string[] {
|
|
1227
1343
|
const tags: string[] = [];
|
|
1228
1344
|
|
|
@@ -1275,7 +1391,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1275
1391
|
const fileSize = this.getFileSize(filePath);
|
|
1276
1392
|
const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
|
|
1277
1393
|
|
|
1278
|
-
logger.
|
|
1394
|
+
logger.debug(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
|
|
1279
1395
|
|
|
1280
1396
|
const baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
|
|
1281
1397
|
|
|
@@ -1307,7 +1423,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1307
1423
|
const { uploadUrl, s3Key, metadata } = presignedResponse.data;
|
|
1308
1424
|
|
|
1309
1425
|
// Step 2: Upload directly to S3 using presigned URL
|
|
1310
|
-
logger.
|
|
1426
|
+
logger.debug(`[UPLOAD] [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
|
|
1311
1427
|
|
|
1312
1428
|
const fileBuffer = fs.readFileSync(filePath);
|
|
1313
1429
|
|
|
@@ -1329,7 +1445,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1329
1445
|
throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
|
|
1330
1446
|
}
|
|
1331
1447
|
|
|
1332
|
-
logger.
|
|
1448
|
+
logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
|
|
1333
1449
|
|
|
1334
1450
|
// Step 3: Confirm upload with server to save metadata
|
|
1335
1451
|
const confirmEndpoint = `${baseUrl}/api/v1/artifacts/public/confirm-upload`;
|
|
@@ -1355,7 +1471,7 @@ export class TestLensReporter implements Reporter {
|
|
|
1355
1471
|
|
|
1356
1472
|
if (confirmResponse.status === 201 && confirmResponse.data.success) {
|
|
1357
1473
|
const artifact = confirmResponse.data.artifact;
|
|
1358
|
-
logger.
|
|
1474
|
+
logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
|
|
1359
1475
|
return {
|
|
1360
1476
|
key: s3Key,
|
|
1361
1477
|
url: artifact.s3Url,
|
|
@@ -1469,4 +1585,3 @@ export class TestLensReporter implements Reporter {
|
|
|
1469
1585
|
}
|
|
1470
1586
|
|
|
1471
1587
|
export default TestLensReporter;
|
|
1472
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alternative-path/testlens-playwright-reporter",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"description": "Universal Playwright reporter for TestLens - works with both TypeScript and JavaScript projects",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
package/postinstall.js
CHANGED
|
@@ -1,44 +1,49 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const pino = require('pino');
|
|
4
|
-
|
|
5
|
-
const logger = pino({
|
|
6
|
-
level: process.env.LOG_LEVEL || 'info'
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const pino = require('pino');
|
|
4
|
+
|
|
5
|
+
const logger = pino({
|
|
6
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
7
|
+
base: null,
|
|
8
|
+
timestamp: false,
|
|
9
|
+
formatters: {
|
|
10
|
+
level: () => ({})
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
// Get the parent project's node_modules/.bin directory
|
|
16
|
+
// When installed, we are in: project/node_modules/testlens-playwright-reporter/
|
|
17
|
+
// We need to go to: project/node_modules/.bin/
|
|
18
|
+
const parentBinDir = path.resolve(__dirname, '..', '.bin');
|
|
19
|
+
const localBinDir = path.resolve(__dirname, 'node_modules', '.bin');
|
|
20
|
+
|
|
21
|
+
// Check if parent bin directory exists
|
|
22
|
+
if (!fs.existsSync(parentBinDir)) {
|
|
23
|
+
logger.info('Parent .bin directory not found, skipping cross-env setup');
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Check if cross-env exists in our node_modules
|
|
28
|
+
const crossEnvCmd = process.platform === 'win32' ? 'cross-env.cmd' : 'cross-env';
|
|
29
|
+
const crossEnvPs1 = 'cross-env.ps1';
|
|
30
|
+
const sourceCmdPath = path.join(localBinDir, crossEnvCmd);
|
|
31
|
+
const sourcePs1Path = path.join(localBinDir, crossEnvPs1);
|
|
32
|
+
|
|
33
|
+
if (fs.existsSync(sourceCmdPath)) {
|
|
34
|
+
const targetCmdPath = path.join(parentBinDir, crossEnvCmd);
|
|
35
|
+
|
|
36
|
+
// Copy the file
|
|
37
|
+
fs.copyFileSync(sourceCmdPath, targetCmdPath);
|
|
38
|
+
logger.info('✓ cross-env binary installed');
|
|
39
|
+
|
|
40
|
+
// Also copy PowerShell script on Windows
|
|
41
|
+
if (process.platform === 'win32' && fs.existsSync(sourcePs1Path)) {
|
|
42
|
+
const targetPs1Path = path.join(parentBinDir, crossEnvPs1);
|
|
43
|
+
fs.copyFileSync(sourcePs1Path, targetPs1Path);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
// Don't fail installation if this doesn't work
|
|
48
|
+
logger.info('Note: Could not setup cross-env automatically:', error.message);
|
|
49
|
+
}
|