UNPKG

@alphabin/trx

Version:

TRX reporter for Playwright tests with Azure Blob Storage upload support

679 lines (678 loc) 27.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ReportDiscoveryService = void 0; const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const logger_util_1 = __importDefault(require("../utils/logger.util")); const config_parser_util_1 = require("../utils/config-parser.util"); /** * Comprehensive service for discovering and processing all types of Playwright reports */ class ReportDiscoveryService { constructor(options) { this.rootDir = options.rootDir; this.playwrightConfigPath = options.playwrightConfigPath; this.configParser = new config_parser_util_1.PlaywrightConfigParser(this.rootDir); this.useLatest = options.useLatest ?? true; this.includeTraces = options.includeTraces ?? true; } /** * Discovers all report types independently */ async discoverAllReports() { logger_util_1.default.debug('Starting comprehensive report discovery'); try { // Parse config first to get expected locations const config = await this.configParser.analyzeConfig(); logger_util_1.default.debug('Config analysis result:', { hasHtmlReporter: config.hasHtmlReporter, hasJsonReporter: config.hasJsonReporter, htmlOutputDir: config.htmlOutputDir, jsonOutputFile: config.jsonOutputFile }); // Discover each report type independently const [htmlReport, jsonReport, blobReport] = await Promise.all([ this.discoverHtmlReport(config), this.discoverJsonReport(config), this.discoverBlobReport(config) ]); const result = { html: htmlReport, json: jsonReport, blob: blobReport, config }; logger_util_1.default.debug('Discovery completed:', { htmlFound: htmlReport.found, jsonFound: jsonReport.found, blobFound: blobReport.found }); return result; } catch (error) { logger_util_1.default.error('Error during report discovery', error); // Return empty result on error return { html: { type: 'html', found: false }, json: { type: 'json', found: false }, blob: { type: 'blob', found: false }, config: { hasHtmlReporter: false, hasJsonReporter: false, hasBlobReporter: false, reporters: [] } }; } } /** * Legacy method: Discovers the main report directory to upload (for backward compatibility) */ async discoverReportDirectory() { try { logger_util_1.default.debug('Using legacy discoverReportDirectory method'); // Use new discovery logic internally const result = await this.discoverAllReports(); // Return HTML report in legacy format for backward compatibility if (result.html.found && result.html.files) { logger_util_1.default.debug(`Converting result to legacy format: ${result.html.path}`); return { path: result.html.path, type: 'html', files: result.html.files, hasIndexHtml: result.html.hasIndexHtml || false }; } logger_util_1.default.debug('No HTML report found for legacy format'); return null; } catch (error) { logger_util_1.default.error('Error in legacy report discovery', error); return null; } } /** * Gets the JSON report file path */ async getJsonReportPath() { try { const result = await this.discoverAllReports(); return result.json.found ? result.json.path || null : null; } catch (error) { logger_util_1.default.error('Error discovering JSON report', error); return null; } } /** * Gets the HTML report directory */ async getHtmlReportDirectory() { try { const result = await this.discoverAllReports(); if (result.html.found && result.html.files) { return { path: result.html.path, type: 'html', files: result.html.files, hasIndexHtml: result.html.hasIndexHtml || false }; } return null; } catch (error) { logger_util_1.default.error('Error discovering HTML report', error); return null; } } /** * Discovers HTML reports using multiple strategies */ async discoverHtmlReport(config) { const result = { type: 'html', found: false }; try { // Strategy 1: Use config-specified directory if (config.hasHtmlReporter && config.htmlOutputDir) { const htmlResult = await this.scanHtmlDirectory(config.htmlOutputDir); if (htmlResult.found) { logger_util_1.default.debug(`HTML report found via config: ${config.htmlOutputDir}`); return htmlResult; } } // Strategy 2: Search default directories const defaultDirs = this.getDefaultHtmlDirectories(); for (const dir of defaultDirs) { const htmlResult = await this.scanHtmlDirectory(dir); if (htmlResult.found) { logger_util_1.default.debug(`HTML report found in default directory: ${dir}`); return htmlResult; } } // Strategy 3: Pattern-based search in root directory const patternResult = await this.searchHtmlByPattern(); if (patternResult.found) { logger_util_1.default.debug(`HTML report found via pattern search: ${patternResult.path}`); return patternResult; } // Strategy 4: Aggressive search for any directory with index.html const aggressiveResult = await this.aggressiveHtmlSearch(); if (aggressiveResult.found) { logger_util_1.default.debug(`HTML report found via aggressive search: ${aggressiveResult.path}`); return aggressiveResult; } logger_util_1.default.debug('No HTML report found'); return result; } catch (error) { logger_util_1.default.debug('Error discovering HTML report', error); return result; } } /** * Discovers JSON reports using multiple strategies */ async discoverJsonReport(config) { const result = { type: 'json', found: false }; try { // Strategy 1: Use config-specified file if (config.hasJsonReporter && config.jsonOutputFile) { const jsonResult = await this.scanJsonFile(config.jsonOutputFile); if (jsonResult.found) { logger_util_1.default.debug(`JSON report found via config: ${config.jsonOutputFile}`); return jsonResult; } } // Strategy 2: Search default locations (including playwright-report) const defaultFiles = this.getDefaultJsonFiles(); for (const file of defaultFiles) { const jsonResult = await this.scanJsonFile(file); if (jsonResult.found) { logger_util_1.default.debug(`JSON report found in default location: ${file}`); return jsonResult; } } // Strategy 3: Pattern-based search (more aggressive) const patternResult = await this.searchJsonByPattern(); if (patternResult.found) { logger_util_1.default.debug(`JSON report found via pattern search: ${patternResult.path}`); return patternResult; } // Strategy 4: Aggressive file search (fallback for any JSON in common dirs) const aggressiveResult = await this.aggressiveJsonSearch(); if (aggressiveResult.found) { logger_util_1.default.debug(`JSON report found via aggressive search: ${aggressiveResult.path}`); return aggressiveResult; } logger_util_1.default.debug('No JSON report found'); return result; } catch (error) { logger_util_1.default.debug('Error discovering JSON report', error); return result; } } /** * Discovers Blob reports using multiple strategies */ async discoverBlobReport(config) { const result = { type: 'blob', found: false }; try { if (!config.hasBlobReporter) { logger_util_1.default.debug('Blob reporter not configured, skipping discovery'); return result; } // Strategy 1: Use config-specified directory if (config.blobOutputDir) { const blobResult = await this.scanBlobDirectory(config.blobOutputDir); if (blobResult.found) { logger_util_1.default.debug(`Blob report found via config: ${config.blobOutputDir}`); return blobResult; } } // Strategy 2: Search default directories const defaultDirs = this.getDefaultBlobDirectories(); for (const dir of defaultDirs) { const blobResult = await this.scanBlobDirectory(dir); if (blobResult.found) { logger_util_1.default.debug(`Blob report found in default directory: ${dir}`); return blobResult; } } logger_util_1.default.debug('No Blob report found'); return result; } catch (error) { logger_util_1.default.debug('Error discovering Blob report', error); return result; } } /** * Scans a directory for HTML reports */ async scanHtmlDirectory(dirPath) { const result = { type: 'html', found: false }; try { if (!this.directoryExists(dirPath)) { return result; } const files = []; let hasIndexHtml = false; let latestModified; await this.scanDirectoryRecursive(dirPath, dirPath, files); // Check for index.html and get latest modification time for (const file of files) { if (file.name === 'index.html' || file.name.endsWith('/index.html')) { hasIndexHtml = true; } const fileStat = fs_1.default.statSync(file.path); if (!latestModified || fileStat.mtime > latestModified) { latestModified = fileStat.mtime; } } if (hasIndexHtml && files.length > 0) { result.found = true; result.path = dirPath; result.files = files; result.hasIndexHtml = hasIndexHtml; result.lastModified = latestModified; result.size = files.reduce((total, file) => total + file.size, 0); } return result; } catch (error) { logger_util_1.default.debug(`Error scanning HTML directory ${dirPath}`, error); return result; } } /** * Scans for a JSON report file */ async scanJsonFile(filePath) { const result = { type: 'json', found: false }; try { if (!fs_1.default.existsSync(filePath)) { return result; } const stats = fs_1.default.statSync(filePath); // Validate it's a valid JSON file const content = fs_1.default.readFileSync(filePath, 'utf-8'); JSON.parse(content); // Will throw if invalid JSON result.found = true; result.path = filePath; result.lastModified = stats.mtime; result.size = stats.size; return result; } catch (error) { logger_util_1.default.debug(`Error scanning JSON file ${filePath}`, error); return result; } } /** * Scans a directory for Blob reports */ async scanBlobDirectory(dirPath) { const result = { type: 'blob', found: false }; try { if (!this.directoryExists(dirPath)) { return result; } const files = []; await this.scanDirectoryRecursive(dirPath, dirPath, files); // Check for blob files (typically .zip files) const blobFiles = files.filter(file => file.name.endsWith('.zip') || file.name.includes('blob')); if (blobFiles.length > 0) { const latestFile = this.useLatest ? this.getLatestFile(blobFiles) : blobFiles[0]; result.found = true; result.path = dirPath; result.files = blobFiles; result.lastModified = new Date(fs_1.default.statSync(latestFile.path).mtime); result.size = blobFiles.reduce((total, file) => total + file.size, 0); } return result; } catch (error) { logger_util_1.default.debug(`Error scanning Blob directory ${dirPath}`, error); return result; } } /** * Searches for HTML reports using patterns */ async searchHtmlByPattern() { const result = { type: 'html', found: false }; try { const entries = fs_1.default.readdirSync(this.rootDir, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { const fullPath = path_1.default.join(this.rootDir, entry.name); const indexPath = path_1.default.join(fullPath, 'index.html'); if (fs_1.default.existsSync(indexPath)) { const htmlResult = await this.scanHtmlDirectory(fullPath); if (htmlResult.found) { return htmlResult; } } } } return result; } catch (error) { logger_util_1.default.debug('Error in pattern-based HTML search', error); return result; } } /** * Searches for JSON reports using patterns */ async searchJsonByPattern() { const result = { type: 'json', found: false }; try { const patterns = ['report.json', 'results.json', 'test-results.json']; const searchDirs = [this.rootDir, ...this.getDefaultJsonDirectories()]; for (const dir of searchDirs) { if (!this.directoryExists(dir)) continue; for (const pattern of patterns) { const filePath = path_1.default.join(dir, pattern); const jsonResult = await this.scanJsonFile(filePath); if (jsonResult.found) { return jsonResult; } } } return result; } catch (error) { logger_util_1.default.debug('Error in pattern-based JSON search', error); return result; } } /** * Recursively scans directory and collects file information */ async scanDirectoryRecursive(currentPath, basePath, files) { try { const items = fs_1.default.readdirSync(currentPath); for (const item of items) { const fullPath = path_1.default.join(currentPath, item); const stats = fs_1.default.statSync(fullPath); if (stats.isDirectory()) { await this.scanDirectoryRecursive(fullPath, basePath, files); } else if (stats.isFile()) { const relativePath = path_1.default.relative(basePath, fullPath); const normalizedPath = relativePath.replace(/\\/g, '/'); const fileInfo = { name: normalizedPath, path: fullPath, contentType: this.getContentType(item), size: stats.size }; files.push(fileInfo); } } } catch (error) { logger_util_1.default.debug(`Error scanning directory ${currentPath}`, error); } } /** * Gets content type for a file */ getContentType(fileName) { const ext = path_1.default.extname(fileName).toLowerCase().substring(1); const mimeMap = { 'html': 'text/html', 'css': 'text/css', 'js': 'application/javascript', 'json': 'application/json', 'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'gif': 'image/gif', 'txt': 'text/plain', 'zip': 'application/zip', 'webm': 'video/webm', 'mp4': 'video/mp4' }; return mimeMap[ext] || 'application/octet-stream'; } /** * Gets the latest file by modification time */ getLatestFile(files) { return files.reduce((latest, current) => { const latestStats = fs_1.default.statSync(latest.path); const currentStats = fs_1.default.statSync(current.path); return currentStats.mtime > latestStats.mtime ? current : latest; }); } /** * Gets default HTML report directories */ getDefaultHtmlDirectories() { return [ path_1.default.join(this.rootDir, 'playwright-report'), path_1.default.join(this.rootDir, 'html-report'), path_1.default.join(this.rootDir, 'reports', 'html'), path_1.default.join(this.rootDir, 'test-results', 'html') ].filter(dir => this.directoryExists(dir)); } /** * Gets default JSON report files */ getDefaultJsonFiles() { return [ // Most common locations path_1.default.join(this.rootDir, 'playwright-report', 'report.json'), // Your specific case path_1.default.join(this.rootDir, 'test-results', 'results.json'), path_1.default.join(this.rootDir, 'test-results', 'report.json'), path_1.default.join(this.rootDir, 'playwright-report', 'results.json'), path_1.default.join(this.rootDir, 'reports', 'report.json'), path_1.default.join(this.rootDir, 'report.json'), path_1.default.join(this.rootDir, 'results.json') ]; } /** * Gets default JSON report directories for pattern search */ getDefaultJsonDirectories() { return [ path_1.default.join(this.rootDir, 'test-results'), path_1.default.join(this.rootDir, 'playwright-report'), path_1.default.join(this.rootDir, 'reports') ].filter(dir => this.directoryExists(dir)); } /** * Gets default Blob report directories */ getDefaultBlobDirectories() { return [ path_1.default.join(this.rootDir, 'blob-report'), path_1.default.join(this.rootDir, 'test-results', 'blob'), path_1.default.join(this.rootDir, 'reports', 'blob') ].filter(dir => this.directoryExists(dir)); } /** * Aggressive search for JSON files in any reasonable location */ async aggressiveJsonSearch() { const result = { type: 'json', found: false }; try { // console.log('[@alphabin/trx] Performing aggressive JSON search...'); // Search common directories for any JSON files that look like reports const searchDirs = [ this.rootDir, path_1.default.join(this.rootDir, 'playwright-report'), path_1.default.join(this.rootDir, 'test-results'), path_1.default.join(this.rootDir, 'reports'), path_1.default.join(this.rootDir, 'blob-report') ]; for (const dir of searchDirs) { if (!this.directoryExists(dir)) continue; // console.log(`[@alphabin/trx] Searching directory: ${dir}`); try { const files = fs_1.default.readdirSync(dir); for (const file of files) { if (file.endsWith('.json')) { const fullPath = path_1.default.join(dir, file); // console.log(`[@alphabin/trx] Found JSON file: ${fullPath}`); const jsonResult = await this.scanJsonFile(fullPath); if (jsonResult.found) { // Additional validation: check if it looks like a Playwright report if (await this.validatePlaywrightJsonReport(fullPath)) { // console.log(`[@alphabin/trx] Validated as Playwright report: ${fullPath}`); return jsonResult; } } } } } catch (error) { console.log(`[@alphabin/trx] Error reading directory ${dir}:`, error); } } return result; } catch (error) { logger_util_1.default.debug('Error in aggressive JSON search', error); return result; } } /** * Aggressive search for HTML reports in any reasonable location */ async aggressiveHtmlSearch() { const result = { type: 'html', found: false }; try { // console.log('[@alphabin/trx] Performing aggressive HTML search...'); // Search common directories for any directory with index.html const searchDirs = [ this.rootDir, path_1.default.join(this.rootDir, 'playwright-report'), path_1.default.join(this.rootDir, 'test-results'), path_1.default.join(this.rootDir, 'reports'), path_1.default.join(this.rootDir, 'html-report') ]; for (const dir of searchDirs) { if (!this.directoryExists(dir)) continue; // console.log(`[@alphabin/trx] Searching directory: ${dir}`); try { // Check if this directory itself has index.html const indexPath = path_1.default.join(dir, 'index.html'); if (fs_1.default.existsSync(indexPath)) { // console.log(`[@alphabin/trx] Found index.html in: ${dir}`); const htmlResult = await this.scanHtmlDirectory(dir); if (htmlResult.found) { return htmlResult; } } // Check subdirectories const items = fs_1.default.readdirSync(dir, { withFileTypes: true }); for (const item of items) { if (item.isDirectory()) { const subDir = path_1.default.join(dir, item.name); const subIndexPath = path_1.default.join(subDir, 'index.html'); if (fs_1.default.existsSync(subIndexPath)) { // console.log(`[@alphabin/trx] Found index.html in subdirectory: ${subDir}`); const htmlResult = await this.scanHtmlDirectory(subDir); if (htmlResult.found) { return htmlResult; } } } } } catch (error) { console.log(`[@alphabin/trx] Error reading directory ${dir}:`, error); } } return result; } catch (error) { logger_util_1.default.debug('Error in aggressive HTML search', error); return result; } } /** * Validates if a JSON file is a Playwright report */ async validatePlaywrightJsonReport(filePath) { try { const content = fs_1.default.readFileSync(filePath, 'utf-8'); const data = JSON.parse(content); // Basic validation: should have config and suites return !!(data.config && data.suites && Array.isArray(data.suites)); } catch { return false; } } /** * Checks if directory exists */ directoryExists(dirPath) { try { const stats = fs_1.default.statSync(dirPath); return stats.isDirectory(); } catch { return false; } } /** * Validates files against allowed types for upload */ validateFiles(files, allowedTypes) { return files.filter(file => { const fileName = path_1.default.basename(file.name); const extension = path_1.default.extname(fileName).toLowerCase().substring(1); if (!allowedTypes.includes(extension)) { logger_util_1.default.debug(`Skipping file with disallowed extension: ${fileName}`); return false; } return true; }); } /** * Gets total size of files */ getTotalSize(files) { return files.reduce((total, file) => total + file.size, 0); } /** * Filters files by maximum size */ filterFilesBySize(files, maxTotalSize) { let currentSize = 0; const result = []; // Sort by priority: HTML files first, then others by size const sortedFiles = [...files].sort((a, b) => { const aIsHtml = path_1.default.extname(a.name).toLowerCase() === '.html'; const bIsHtml = path_1.default.extname(b.name).toLowerCase() === '.html'; if (aIsHtml && !bIsHtml) return -1; if (!aIsHtml && bIsHtml) return 1; return a.size - b.size; // Smaller files first }); for (const file of sortedFiles) { if (currentSize + file.size <= maxTotalSize) { result.push(file); currentSize += file.size; } else { logger_util_1.default.debug(`Skipping file due to size limit: ${file.name} (${file.size} bytes)`); } } return result; } } exports.ReportDiscoveryService = ReportDiscoveryService; exports.default = ReportDiscoveryService;