UNPKG

tdpw

Version:

CLI tool for uploading Playwright test reports to TestDino platform with TestDino storage support

440 lines 16 kB
"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