tdpw
Version:
CLI tool for uploading Playwright test reports to TestDino platform with TestDino storage support
440 lines • 16 kB
JavaScript
"use strict";
/**
* Attachment processing utilities for Playwright reports
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.AttachmentScanner = void 0;
const path_1 = require("path");
const fs_1 = require("../utils/fs");
/**
* Content type mappings for attachment classification
*/
const CONTENT_TYPE_MAPPING = {
image: [
'image/png',
'image/jpeg',
'image/jpg',
'image/gif',
'image/webp',
'image/svg+xml',
'image/bmp',
'image/tiff',
],
video: ['video/webm', 'video/mp4', 'video/avi', 'video/mov', 'video/mkv'],
trace: [
'application/zip',
'application/x-zip-compressed',
'application/octet-stream', // Common for .trace files
],
file: [
'text/markdown',
'text/plain',
'application/pdf',
'text/x-log',
'application/octet-stream', // Sometimes used for .log files
],
};
/**
* Service for scanning and processing attachments from Playwright reports
*/
class AttachmentScanner {
baseDirectory;
constructor(baseDirectory) {
this.baseDirectory = baseDirectory;
}
/**
* Scan Playwright report for all attachments
*/
async scanAttachments(report) {
const attachments = [];
// Recursively scan all test suites and specs
for (const suite of report.suites || []) {
await this.scanSuite(suite, attachments);
}
// Classify attachments by type
const result = {
images: [],
videos: [],
traces: [],
files: [],
other: [],
total: attachments.length,
};
for (const attachment of attachments) {
switch (attachment.type) {
case 'image':
result.images.push(attachment);
break;
case 'video':
result.videos.push(attachment);
break;
case 'trace':
result.traces.push(attachment);
break;
case 'file':
result.files.push(attachment);
break;
default:
result.other.push(attachment);
break;
}
}
return result;
}
/**
* Recursively scan a test suite for attachments
*/
async scanSuite(suite, attachments) {
const suiteRecord = suite;
// Scan specs in this suite
if (Array.isArray(suiteRecord.specs)) {
for (const spec of suiteRecord.specs) {
await this.scanSpec(spec, attachments);
}
}
// Scan nested suites
if (Array.isArray(suiteRecord.suites)) {
for (const nestedSuite of suiteRecord.suites) {
await this.scanSuite(nestedSuite, attachments);
}
}
}
/**
* Scan a spec for attachments
*/
async scanSpec(spec, attachments) {
const specRecord = spec;
if (!Array.isArray(specRecord.tests))
return;
for (const test of specRecord.tests) {
const testRecord = test;
if (!Array.isArray(testRecord.results))
continue;
for (const result of testRecord.results) {
const resultRecord = result;
if (Array.isArray(resultRecord.attachments)) {
for (const attachment of resultRecord.attachments) {
const attachmentInfo = await this.processAttachment(attachment);
if (attachmentInfo) {
attachments.push(attachmentInfo);
}
}
}
}
}
}
/**
* Process a single attachment and resolve its paths
*/
async processAttachment(attachment) {
if (!attachment || typeof attachment !== 'object') {
return null;
}
const attachmentRecord = attachment;
const { name, contentType, path } = attachmentRecord;
if (!name ||
!contentType ||
!path ||
typeof name !== 'string' ||
typeof contentType !== 'string' ||
typeof path !== 'string') {
return null;
}
// Resolve paths
const absolutePath = (0, path_1.isAbsolute)(path)
? path
: (0, path_1.resolve)(this.baseDirectory, path);
// Check if file exists
if (!(await (0, fs_1.exists)(absolutePath))) {
console.warn(`⚠️ Attachment file not found: ${absolutePath}`);
return null;
}
// Calculate relative path from base directory
const relativePath = (0, path_1.relative)(this.baseDirectory, absolutePath);
// Determine attachment type
const type = this.classifyAttachment(contentType, name);
return {
name,
contentType,
originalPath: path,
relativePath,
absolutePath,
type,
};
}
/**
* Classify attachment by content type and name
*/
classifyAttachment(contentType, name) {
const lowerContentType = contentType.toLowerCase();
const lowerName = name.toLowerCase();
// Check content type mappings
if (CONTENT_TYPE_MAPPING.image.includes(lowerContentType)) {
return 'image';
}
if (CONTENT_TYPE_MAPPING.video.includes(lowerContentType)) {
return 'video';
}
if (CONTENT_TYPE_MAPPING.trace.includes(lowerContentType)) {
// Additional check for trace files by name
if (lowerName.includes('trace') ||
lowerName.endsWith('.trace') ||
lowerName.endsWith('.zip')) {
return 'trace';
}
}
if (CONTENT_TYPE_MAPPING.file.includes(lowerContentType)) {
return 'file';
}
// Fallback classification by file extension
if (lowerName.endsWith('.png') ||
lowerName.endsWith('.jpg') ||
lowerName.endsWith('.jpeg') ||
lowerName.endsWith('.gif') ||
lowerName.endsWith('.webp') ||
lowerName.endsWith('.svg')) {
return 'image';
}
if (lowerName.endsWith('.webm') ||
lowerName.endsWith('.mp4') ||
lowerName.endsWith('.avi') ||
lowerName.endsWith('.mov')) {
return 'video';
}
if (lowerName.includes('trace') ||
lowerName.endsWith('.trace') ||
lowerName.endsWith('.zip')) {
return 'trace';
}
if (lowerName.endsWith('.md') ||
lowerName.endsWith('.pdf') ||
lowerName.endsWith('.txt') ||
lowerName.endsWith('.log')) {
return 'file';
}
return 'other';
}
/**
* Filter attachments based on configuration flags
*/
static filterAttachments(scanResult, config) {
const filtered = [];
// uploadFullJson overrides all other flags
if (config.uploadFullJson) {
filtered.push(...scanResult.images);
filtered.push(...scanResult.videos);
filtered.push(...scanResult.files);
return filtered;
}
// HTML flag includes images and videos
if (config.uploadHtml) {
filtered.push(...scanResult.images);
filtered.push(...scanResult.videos);
}
// Individual flags (can be combined with HTML flag)
if (config.uploadImages && !config.uploadHtml) {
filtered.push(...scanResult.images);
}
if (config.uploadVideos && !config.uploadHtml) {
filtered.push(...scanResult.videos);
}
if (config.uploadFiles) {
filtered.push(...scanResult.files);
}
if (config.uploadTraces) {
filtered.push(...scanResult.traces);
}
return filtered;
}
/**
* Static version of classifyAttachment for use in static methods
*/
static classifyAttachmentType(contentType, name) {
const lowerContentType = contentType.toLowerCase();
const lowerName = name.toLowerCase();
// Check content type mappings
if (CONTENT_TYPE_MAPPING.image.includes(lowerContentType)) {
return 'image';
}
if (CONTENT_TYPE_MAPPING.video.includes(lowerContentType)) {
return 'video';
}
if (CONTENT_TYPE_MAPPING.trace.includes(lowerContentType)) {
// Additional check for trace files by name
if (lowerName.includes('trace') ||
lowerName.endsWith('.trace') ||
lowerName.endsWith('.zip')) {
return 'trace';
}
}
if (CONTENT_TYPE_MAPPING.file.includes(lowerContentType)) {
return 'file';
}
// Fallback classification by file extension
if (lowerName.endsWith('.png') ||
lowerName.endsWith('.jpg') ||
lowerName.endsWith('.jpeg') ||
lowerName.endsWith('.gif') ||
lowerName.endsWith('.webp') ||
lowerName.endsWith('.svg')) {
return 'image';
}
if (lowerName.endsWith('.webm') ||
lowerName.endsWith('.mp4') ||
lowerName.endsWith('.avi') ||
lowerName.endsWith('.mov')) {
return 'video';
}
if (lowerName.includes('trace') ||
lowerName.endsWith('.trace') ||
lowerName.endsWith('.zip')) {
return 'trace';
}
if (lowerName.endsWith('.md') ||
lowerName.endsWith('.pdf') ||
lowerName.endsWith('.txt') ||
lowerName.endsWith('.log')) {
return 'file';
}
return 'other';
}
/**
* Determine what should happen to an attachment path based on config
* Returns: 'upload' | 'not-enabled' | 'not-supported' | 'unchanged'
*/
static getAttachmentPathAction(attachmentRecord, config) {
const contentType = attachmentRecord.contentType || '';
const name = attachmentRecord.name || '';
// Classify the attachment type using the same logic as classifyAttachment
const type = this.classifyAttachmentType(contentType, name);
// uploadFullJson overrides everything
if (config.uploadFullJson) {
switch (type) {
case 'image':
case 'video':
case 'file':
return 'upload';
case 'trace':
return 'not-supported';
default:
return 'unchanged';
}
}
// Determine action based on type and config
switch (type) {
case 'image':
return config.uploadImages || config.uploadHtml
? 'upload'
: 'not-enabled';
case 'video':
return config.uploadVideos || config.uploadHtml
? 'upload'
: 'not-enabled';
case 'file':
return config.uploadFiles ? 'upload' : 'not-enabled';
case 'trace':
return config.uploadTraces ? 'upload' : 'not-enabled';
default:
// Other attachment types are not processed
return 'unchanged';
}
}
/**
* Update attachment paths in the JSON report to use Azure URLs or mark as 'Not Enabled'
*/
static updateAttachmentPaths(report, urlMapping, config) {
// Deep clone the report to avoid mutations
const updatedReport = JSON.parse(JSON.stringify(report));
// Keep track of statistics for verbose logging
let _uploadedCount = 0;
let _notEnabledCount = 0;
let _unchangedCount = 0;
// Keep track of statistics for verbose logging
let _notSupportedCount = 0;
// Counter function
const countUpdate = (newPath, _originalPath) => {
if (newPath.startsWith('http')) {
_uploadedCount++;
}
else if (newPath === 'Not Enabled') {
_notEnabledCount++;
}
else if (newPath === 'Not Supported') {
_notSupportedCount++;
}
else {
_unchangedCount++;
}
};
// Recursively update all attachment paths
const updateSuite = (suite) => {
if (Array.isArray(suite.specs)) {
for (const spec of suite
.specs) {
updateSpec(spec);
}
}
if (Array.isArray(suite.suites)) {
for (const nestedSuite of suite
.suites) {
updateSuite(nestedSuite);
}
}
};
const updateSpec = (spec) => {
const specRecord = spec;
if (!Array.isArray(specRecord.tests))
return;
for (const test of specRecord.tests) {
const testRecord = test;
if (!Array.isArray(testRecord.results))
continue;
for (const result of testRecord.results) {
const resultRecord = result;
if (Array.isArray(resultRecord.attachments)) {
for (const attachment of resultRecord.attachments) {
const attachmentRecord = attachment;
if (attachmentRecord.path &&
typeof attachmentRecord.path === 'string') {
const originalPath = attachmentRecord.path;
let newPath = originalPath;
// If we have the uploaded URL, use it
if (urlMapping.has(originalPath)) {
newPath = urlMapping.get(originalPath) || originalPath;
attachmentRecord.path = newPath;
}
// If config is provided, determine what action to take
else if (config) {
const action = this.getAttachmentPathAction(attachmentRecord, config);
switch (action) {
case 'not-enabled':
newPath = 'Not Enabled';
attachmentRecord.path = newPath;
break;
case 'not-supported':
newPath = 'Not Supported';
attachmentRecord.path = newPath;
break;
case 'upload':
case 'unchanged':
default:
// Leave the original path unchanged
break;
}
}
countUpdate(newPath, originalPath);
}
}
}
}
}
};
// Update all suites
for (const suite of updatedReport.suites || []) {
updateSuite(suite);
}
return updatedReport;
}
}
exports.AttachmentScanner = AttachmentScanner;
//# sourceMappingURL=attachments.js.map