testlens-playwright-reporter 0.2.4 → 0.2.5
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.js +46 -34
- package/index.ts +46 -16
- package/lib/index.js +45 -15
- package/package.json +1 -1
- package/CHANGELOG.md +0 -23
package/index.js
CHANGED
|
@@ -6,12 +6,23 @@ const path = require('path');
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const https = require('https');
|
|
8
8
|
const axios = require('axios');
|
|
9
|
-
const mime = require('mime');
|
|
10
9
|
const FormData = require('form-data');
|
|
10
|
+
|
|
11
|
+
// Lazy-load mime module to support ESM
|
|
12
|
+
let mimeModule = null;
|
|
13
|
+
async function getMime() {
|
|
14
|
+
if (!mimeModule) {
|
|
15
|
+
const imported = await import('mime');
|
|
16
|
+
// Handle both default export and named exports
|
|
17
|
+
mimeModule = imported.default || imported;
|
|
18
|
+
}
|
|
19
|
+
return mimeModule;
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
class TestLensReporter {
|
|
12
23
|
constructor(options = {}) {
|
|
13
24
|
this.config = {
|
|
14
|
-
apiEndpoint: 'https://testlens.qa-path.com/api/v1/webhook/playwright',
|
|
25
|
+
apiEndpoint: options.apiEndpoint || 'https://testlens.qa-path.com/api/v1/webhook/playwright',
|
|
15
26
|
apiKey: options.apiKey, // API key must come from config file
|
|
16
27
|
enableRealTimeStream: options.enableRealTimeStream !== undefined ? options.enableRealTimeStream : true,
|
|
17
28
|
enableGitInfo: options.enableGitInfo !== undefined ? options.enableGitInfo : true,
|
|
@@ -371,20 +382,14 @@ class TestLensReporter {
|
|
|
371
382
|
try {
|
|
372
383
|
console.log(`📤 Processing ${attachment.name} asynchronously...`);
|
|
373
384
|
|
|
374
|
-
// Preserve original file extension from path (important for traces which must be .zip)
|
|
375
|
-
const originalExt = path.extname(attachment.path);
|
|
376
|
-
const fileName = attachment.name.includes(originalExt)
|
|
377
|
-
? attachment.name
|
|
378
|
-
: attachment.name + originalExt;
|
|
379
|
-
|
|
380
385
|
// Upload to S3 first
|
|
381
|
-
const s3Data = await this.uploadArtifactToS3(attachment.path, testId,
|
|
386
|
+
const s3Data = await this.uploadArtifactToS3(attachment.path, testId, attachment.name);
|
|
382
387
|
|
|
383
388
|
const artifactData = {
|
|
384
389
|
testId,
|
|
385
|
-
type: this.getArtifactType(
|
|
390
|
+
type: this.getArtifactType(attachment.name),
|
|
386
391
|
path: attachment.path,
|
|
387
|
-
name:
|
|
392
|
+
name: attachment.name,
|
|
388
393
|
contentType: attachment.contentType,
|
|
389
394
|
fileSize: this.getFileSize(attachment.path),
|
|
390
395
|
storageType: 's3',
|
|
@@ -579,24 +584,6 @@ class TestLensReporter {
|
|
|
579
584
|
return `${test.location.file}:${test.location.line}:${cleanTitle}`;
|
|
580
585
|
}
|
|
581
586
|
|
|
582
|
-
getContentType(ext) {
|
|
583
|
-
const contentTypes = {
|
|
584
|
-
'.mp4': 'video/mp4',
|
|
585
|
-
'.webm': 'video/webm',
|
|
586
|
-
'.png': 'image/png',
|
|
587
|
-
'.jpg': 'image/jpeg',
|
|
588
|
-
'.jpeg': 'image/jpeg',
|
|
589
|
-
'.gif': 'image/gif',
|
|
590
|
-
'.json': 'application/json',
|
|
591
|
-
'.txt': 'text/plain',
|
|
592
|
-
'.html': 'text/html',
|
|
593
|
-
'.xml': 'application/xml',
|
|
594
|
-
'.zip': 'application/zip',
|
|
595
|
-
'.pdf': 'application/pdf'
|
|
596
|
-
};
|
|
597
|
-
return contentTypes[ext] || 'application/octet-stream';
|
|
598
|
-
}
|
|
599
|
-
|
|
600
587
|
getFileSize(filePath) {
|
|
601
588
|
try {
|
|
602
589
|
const stats = fs.statSync(filePath);
|
|
@@ -624,7 +611,7 @@ class TestLensReporter {
|
|
|
624
611
|
testRunId: this.runId,
|
|
625
612
|
testId: testId,
|
|
626
613
|
fileName: fileName,
|
|
627
|
-
fileType: this.getContentType(fileName),
|
|
614
|
+
fileType: await this.getContentType(fileName),
|
|
628
615
|
fileSize: fileSize,
|
|
629
616
|
artifactType: this.getArtifactType(fileName)
|
|
630
617
|
}, {
|
|
@@ -670,7 +657,7 @@ class TestLensReporter {
|
|
|
670
657
|
testId: testId,
|
|
671
658
|
s3Key: s3Key,
|
|
672
659
|
fileName: fileName,
|
|
673
|
-
fileType: this.getContentType(fileName),
|
|
660
|
+
fileType: await this.getContentType(fileName),
|
|
674
661
|
fileSize: fileSize,
|
|
675
662
|
artifactType: this.getArtifactType(fileName)
|
|
676
663
|
}, {
|
|
@@ -714,10 +701,35 @@ class TestLensReporter {
|
|
|
714
701
|
}
|
|
715
702
|
}
|
|
716
703
|
|
|
717
|
-
getContentType(fileName) {
|
|
704
|
+
async getContentType(fileName) {
|
|
718
705
|
const ext = path.extname(fileName).toLowerCase();
|
|
719
|
-
|
|
720
|
-
|
|
706
|
+
try {
|
|
707
|
+
const mime = await getMime();
|
|
708
|
+
// Try different ways to access getType method
|
|
709
|
+
const getType = mime.getType || mime.default?.getType;
|
|
710
|
+
if (typeof getType === 'function') {
|
|
711
|
+
const mimeType = getType.call(mime, ext) || getType.call(mime.default, ext);
|
|
712
|
+
return mimeType || 'application/octet-stream';
|
|
713
|
+
}
|
|
714
|
+
} catch (error) {
|
|
715
|
+
console.warn(`Failed to get MIME type for ${fileName}:`, error.message);
|
|
716
|
+
}
|
|
717
|
+
// Fallback to basic content type mapping
|
|
718
|
+
const contentTypes = {
|
|
719
|
+
'.mp4': 'video/mp4',
|
|
720
|
+
'.webm': 'video/webm',
|
|
721
|
+
'.png': 'image/png',
|
|
722
|
+
'.jpg': 'image/jpeg',
|
|
723
|
+
'.jpeg': 'image/jpeg',
|
|
724
|
+
'.gif': 'image/gif',
|
|
725
|
+
'.json': 'application/json',
|
|
726
|
+
'.txt': 'text/plain',
|
|
727
|
+
'.html': 'text/html',
|
|
728
|
+
'.xml': 'application/xml',
|
|
729
|
+
'.zip': 'application/zip',
|
|
730
|
+
'.pdf': 'application/pdf'
|
|
731
|
+
};
|
|
732
|
+
return contentTypes[ext] || 'application/octet-stream';
|
|
721
733
|
}
|
|
722
734
|
}
|
|
723
735
|
|
package/index.ts
CHANGED
|
@@ -8,8 +8,19 @@ import * as https from 'https';
|
|
|
8
8
|
import axios, { AxiosInstance } from 'axios';
|
|
9
9
|
import type { Reporter, TestCase, TestResult, FullConfig, Suite } from '@playwright/test/reporter';
|
|
10
10
|
import { execSync } from 'child_process';
|
|
11
|
-
import * as mime from 'mime';
|
|
12
11
|
import FormData from 'form-data';
|
|
12
|
+
|
|
13
|
+
// Lazy-load mime module to support ESM
|
|
14
|
+
let mimeModule: any = null;
|
|
15
|
+
async function getMime() {
|
|
16
|
+
if (!mimeModule) {
|
|
17
|
+
const imported = await import('mime');
|
|
18
|
+
// Handle both default export and named exports
|
|
19
|
+
mimeModule = imported.default || imported;
|
|
20
|
+
}
|
|
21
|
+
return mimeModule;
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
export interface TestLensReporterConfig {
|
|
14
25
|
/** TestLens API endpoint URL */
|
|
15
26
|
apiEndpoint?: string;
|
|
@@ -483,26 +494,20 @@ export class TestLensReporter implements Reporter {
|
|
|
483
494
|
for (const attachment of attachments) {
|
|
484
495
|
if (attachment.path) {
|
|
485
496
|
try {
|
|
486
|
-
// Preserve original file extension from path (important for traces which must be .zip)
|
|
487
|
-
const originalExt = path.extname(attachment.path);
|
|
488
|
-
const fileName = attachment.name.includes(originalExt)
|
|
489
|
-
? attachment.name
|
|
490
|
-
: attachment.name + originalExt;
|
|
491
|
-
|
|
492
497
|
// Upload to S3 first
|
|
493
|
-
const s3Data = await this.uploadArtifactToS3(attachment.path, testId,
|
|
498
|
+
const s3Data = await this.uploadArtifactToS3(attachment.path, testId, attachment.name);
|
|
494
499
|
|
|
495
500
|
// Skip if upload failed or file was too large
|
|
496
501
|
if (!s3Data) {
|
|
497
|
-
console.log(`⏭️ Skipping artifact ${
|
|
502
|
+
console.log(`⏭️ Skipping artifact ${attachment.name} - upload failed or file too large`);
|
|
498
503
|
continue;
|
|
499
504
|
}
|
|
500
505
|
|
|
501
506
|
const artifactData = {
|
|
502
507
|
testId,
|
|
503
|
-
type: this.getArtifactType(
|
|
508
|
+
type: this.getArtifactType(attachment.name),
|
|
504
509
|
path: attachment.path,
|
|
505
|
-
name:
|
|
510
|
+
name: attachment.name,
|
|
506
511
|
contentType: attachment.contentType,
|
|
507
512
|
fileSize: this.getFileSize(attachment.path),
|
|
508
513
|
storageType: 's3',
|
|
@@ -711,7 +716,7 @@ export class TestLensReporter implements Reporter {
|
|
|
711
716
|
testRunId: this.runId,
|
|
712
717
|
testId: testId,
|
|
713
718
|
fileName: fileName,
|
|
714
|
-
fileType: this.getContentType(fileName),
|
|
719
|
+
fileType: await this.getContentType(fileName),
|
|
715
720
|
fileSize: fileSize,
|
|
716
721
|
artifactType: this.getArtifactType(fileName)
|
|
717
722
|
}, {
|
|
@@ -757,7 +762,7 @@ export class TestLensReporter implements Reporter {
|
|
|
757
762
|
testId: testId,
|
|
758
763
|
s3Key: s3Key,
|
|
759
764
|
fileName: fileName,
|
|
760
|
-
fileType: this.getContentType(fileName),
|
|
765
|
+
fileType: await this.getContentType(fileName),
|
|
761
766
|
fileSize: fileSize,
|
|
762
767
|
artifactType: this.getArtifactType(fileName)
|
|
763
768
|
}, {
|
|
@@ -801,10 +806,35 @@ export class TestLensReporter implements Reporter {
|
|
|
801
806
|
}
|
|
802
807
|
}
|
|
803
808
|
|
|
804
|
-
private getContentType(fileName: string): string {
|
|
809
|
+
private async getContentType(fileName: string): Promise<string> {
|
|
805
810
|
const ext = path.extname(fileName).toLowerCase();
|
|
806
|
-
|
|
807
|
-
|
|
811
|
+
try {
|
|
812
|
+
const mime = await getMime();
|
|
813
|
+
// Try different ways to access getType method
|
|
814
|
+
const getType = mime.getType || mime.default?.getType;
|
|
815
|
+
if (typeof getType === 'function') {
|
|
816
|
+
const mimeType = getType.call(mime, ext) || getType.call(mime.default, ext);
|
|
817
|
+
return mimeType || 'application/octet-stream';
|
|
818
|
+
}
|
|
819
|
+
} catch (error: any) {
|
|
820
|
+
console.warn(`Failed to get MIME type for ${fileName}:`, error.message);
|
|
821
|
+
}
|
|
822
|
+
// Fallback to basic content type mapping
|
|
823
|
+
const contentTypes: Record<string, string> = {
|
|
824
|
+
'.mp4': 'video/mp4',
|
|
825
|
+
'.webm': 'video/webm',
|
|
826
|
+
'.png': 'image/png',
|
|
827
|
+
'.jpg': 'image/jpeg',
|
|
828
|
+
'.jpeg': 'image/jpeg',
|
|
829
|
+
'.gif': 'image/gif',
|
|
830
|
+
'.json': 'application/json',
|
|
831
|
+
'.txt': 'text/plain',
|
|
832
|
+
'.html': 'text/html',
|
|
833
|
+
'.xml': 'application/xml',
|
|
834
|
+
'.zip': 'application/zip',
|
|
835
|
+
'.pdf': 'application/pdf'
|
|
836
|
+
};
|
|
837
|
+
return contentTypes[ext] || 'application/octet-stream';
|
|
808
838
|
}
|
|
809
839
|
|
|
810
840
|
private generateS3Key(runId: string, testId: string, fileName: string): string {
|
package/lib/index.js
CHANGED
|
@@ -45,7 +45,16 @@ const fs = __importStar(require("fs"));
|
|
|
45
45
|
const https = __importStar(require("https"));
|
|
46
46
|
const axios_1 = __importDefault(require("axios"));
|
|
47
47
|
const child_process_1 = require("child_process");
|
|
48
|
-
|
|
48
|
+
// Lazy-load mime module to support ESM
|
|
49
|
+
let mimeModule = null;
|
|
50
|
+
async function getMime() {
|
|
51
|
+
if (!mimeModule) {
|
|
52
|
+
const imported = await Promise.resolve().then(() => __importStar(require('mime')));
|
|
53
|
+
// Handle both default export and named exports
|
|
54
|
+
mimeModule = imported.default || imported;
|
|
55
|
+
}
|
|
56
|
+
return mimeModule;
|
|
57
|
+
}
|
|
49
58
|
class TestLensReporter {
|
|
50
59
|
constructor(options) {
|
|
51
60
|
this.config = {
|
|
@@ -352,23 +361,18 @@ class TestLensReporter {
|
|
|
352
361
|
for (const attachment of attachments) {
|
|
353
362
|
if (attachment.path) {
|
|
354
363
|
try {
|
|
355
|
-
// Preserve original file extension from path (important for traces which must be .zip)
|
|
356
|
-
const originalExt = path.extname(attachment.path);
|
|
357
|
-
const fileName = attachment.name.includes(originalExt)
|
|
358
|
-
? attachment.name
|
|
359
|
-
: attachment.name + originalExt;
|
|
360
364
|
// Upload to S3 first
|
|
361
|
-
const s3Data = await this.uploadArtifactToS3(attachment.path, testId,
|
|
365
|
+
const s3Data = await this.uploadArtifactToS3(attachment.path, testId, attachment.name);
|
|
362
366
|
// Skip if upload failed or file was too large
|
|
363
367
|
if (!s3Data) {
|
|
364
|
-
console.log(`⏭️ Skipping artifact ${
|
|
368
|
+
console.log(`⏭️ Skipping artifact ${attachment.name} - upload failed or file too large`);
|
|
365
369
|
continue;
|
|
366
370
|
}
|
|
367
371
|
const artifactData = {
|
|
368
372
|
testId,
|
|
369
|
-
type: this.getArtifactType(
|
|
373
|
+
type: this.getArtifactType(attachment.name),
|
|
370
374
|
path: attachment.path,
|
|
371
|
-
name:
|
|
375
|
+
name: attachment.name,
|
|
372
376
|
contentType: attachment.contentType,
|
|
373
377
|
fileSize: this.getFileSize(attachment.path),
|
|
374
378
|
storageType: 's3',
|
|
@@ -555,7 +559,7 @@ class TestLensReporter {
|
|
|
555
559
|
testRunId: this.runId,
|
|
556
560
|
testId: testId,
|
|
557
561
|
fileName: fileName,
|
|
558
|
-
fileType: this.getContentType(fileName),
|
|
562
|
+
fileType: await this.getContentType(fileName),
|
|
559
563
|
fileSize: fileSize,
|
|
560
564
|
artifactType: this.getArtifactType(fileName)
|
|
561
565
|
}, {
|
|
@@ -593,7 +597,7 @@ class TestLensReporter {
|
|
|
593
597
|
testId: testId,
|
|
594
598
|
s3Key: s3Key,
|
|
595
599
|
fileName: fileName,
|
|
596
|
-
fileType: this.getContentType(fileName),
|
|
600
|
+
fileType: await this.getContentType(fileName),
|
|
597
601
|
fileSize: fileSize,
|
|
598
602
|
artifactType: this.getArtifactType(fileName)
|
|
599
603
|
}, {
|
|
@@ -637,10 +641,36 @@ class TestLensReporter {
|
|
|
637
641
|
return null;
|
|
638
642
|
}
|
|
639
643
|
}
|
|
640
|
-
getContentType(fileName) {
|
|
644
|
+
async getContentType(fileName) {
|
|
641
645
|
const ext = path.extname(fileName).toLowerCase();
|
|
642
|
-
|
|
643
|
-
|
|
646
|
+
try {
|
|
647
|
+
const mime = await getMime();
|
|
648
|
+
// Try different ways to access getType method
|
|
649
|
+
const getType = mime.getType || mime.default?.getType;
|
|
650
|
+
if (typeof getType === 'function') {
|
|
651
|
+
const mimeType = getType.call(mime, ext) || getType.call(mime.default, ext);
|
|
652
|
+
return mimeType || 'application/octet-stream';
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
console.warn(`Failed to get MIME type for ${fileName}:`, error.message);
|
|
657
|
+
}
|
|
658
|
+
// Fallback to basic content type mapping
|
|
659
|
+
const contentTypes = {
|
|
660
|
+
'.mp4': 'video/mp4',
|
|
661
|
+
'.webm': 'video/webm',
|
|
662
|
+
'.png': 'image/png',
|
|
663
|
+
'.jpg': 'image/jpeg',
|
|
664
|
+
'.jpeg': 'image/jpeg',
|
|
665
|
+
'.gif': 'image/gif',
|
|
666
|
+
'.json': 'application/json',
|
|
667
|
+
'.txt': 'text/plain',
|
|
668
|
+
'.html': 'text/html',
|
|
669
|
+
'.xml': 'application/xml',
|
|
670
|
+
'.zip': 'application/zip',
|
|
671
|
+
'.pdf': 'application/pdf'
|
|
672
|
+
};
|
|
673
|
+
return contentTypes[ext] || 'application/octet-stream';
|
|
644
674
|
}
|
|
645
675
|
generateS3Key(runId, testId, fileName) {
|
|
646
676
|
const date = new Date().toISOString().slice(0, 10);
|
package/package.json
CHANGED
package/CHANGELOG.md
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to the testlens-playwright-reporter package will be documented in this file.
|
|
4
|
-
|
|
5
|
-
## [0.2.4] - 2025-11-18
|
|
6
|
-
|
|
7
|
-
### Fixed
|
|
8
|
-
- **TypeScript source updated**: Applied trace file extension preservation fix to TypeScript source (`index.ts`) and rebuilt `lib/index.js` to ensure both TypeScript and JavaScript implementations have the fix.
|
|
9
|
-
|
|
10
|
-
### Fixed
|
|
11
|
-
- **Package entry point**: Changed main entry point from `lib/index.js` to `index.js` to use the correct implementation with trace file extension preservation fix.
|
|
12
|
-
|
|
13
|
-
## [0.2.3] - 2025-11-18
|
|
14
|
-
|
|
15
|
-
### Fixed
|
|
16
|
-
- **Trace file extension preservation**: Fixed issue where Playwright trace files were being uploaded to S3 without their `.zip` extension, causing "Could not load trace" errors in Playwright Trace Viewer. The reporter now preserves the original file extension from `attachment.path` when uploading artifacts, ensuring trace files maintain their `.zip` format required by the viewer.
|
|
17
|
-
|
|
18
|
-
## [0.2.2] - Previous release
|
|
19
|
-
|
|
20
|
-
### Features
|
|
21
|
-
- Direct S3 uploads with presigned URLs
|
|
22
|
-
- Asynchronous artifact processing
|
|
23
|
-
- Enhanced error handling and retry logic
|