UNPKG

@alphabin/trx

Version:

TRX reporter for Playwright tests with Azure Blob Storage upload support

632 lines (631 loc) 31.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TestReportXReporter = void 0; const fs_1 = __importDefault(require("fs")); const config_1 = require("./config"); const collectors_1 = require("./collectors"); const api_util_1 = require("./utils/api.util"); const fileSystem_util_1 = require("./utils/fileSystem.util"); const config_parser_util_1 = require("./utils/config-parser.util"); const report_discovery_service_1 = require("./services/report-discovery.service"); const azure_storage_service_1 = require("./services/azure-storage.service"); const error_handler_util_1 = require("./utils/error-handler.util"); const console_formatter_util_1 = require("./utils/console-formatter.util"); const logger_util_1 = __importDefault(require("./utils/logger.util")); /** * Enhanced TRX reporter for Playwright tests with improved discovery and error handling */ class TestReportXReporter { constructor(options = {}) { try { // Enable debug logging FIRST if requested if (options.debug) { process.env.DEBUG = process.env.DEBUG ? `${process.env.DEBUG},alphabin:trx` : 'alphabin:trx'; console.log('[@alphabin/trx] Debug mode enabled'); } // Validate configuration const configErrors = error_handler_util_1.ErrorHandler.validateConfig(options); if (configErrors.length > 0) { throw new Error(`Configuration errors: ${configErrors.join(', ')}`); } // Initialize configuration this.config = new config_1.Config(options); const configOptions = this.config.get(); // Initialize API client this.apiClient = new api_util_1.ApiClient({ serverUrl: configOptions.serverUrl, apiKey: configOptions.apiKey, timeout: configOptions.timeout, retries: configOptions.retries }); // Initialize Azure storage service this.azureStorageService = new azure_storage_service_1.AzureStorageService({ serverUrl: configOptions.serverUrl, apiKey: configOptions.apiKey, timeout: configOptions.timeout }); // Initialize metadata collector this.metadataCollector = new collectors_1.MetadataCollector({ rootDir: process.cwd(), collectGit: configOptions.collectGitMetadata, collectCI: configOptions.collectCiMetadata, collectSystem: configOptions.collectSystemMetadata, customMetadata: configOptions.customMetadata }); // Initialize enhanced report discovery service this.reportDiscoveryService = new report_discovery_service_1.ReportDiscoveryService({ rootDir: process.cwd(), useLatest: true, includeTraces: true }); // Initialize config parser this.configParser = new config_parser_util_1.PlaywrightConfigParser(process.cwd()); logger_util_1.default.debug('Enhanced TRX reporter initialized successfully'); } catch (error) { const formattedError = error_handler_util_1.ErrorHandler.formatErrorForUser(error, 'TRX Reporter initialization'); console.error(console_formatter_util_1.ConsoleFormatter.format(console_formatter_util_1.ConsoleFormatter.formatError(formattedError))); throw error; } } /** * Called when the test run is starting */ onBegin(config) { logger_util_1.default.debug('Test run starting', { workers: config.workers, rootDir: config.rootDir }); // Keep reference to FullConfig for viewport/headless resolution this.fullConfig = config; logger_util_1.default.debug('Enhanced discovery and validation will be used for report processing'); } /** * Called when the test run is finished */ async onEnd() { logger_util_1.default.debug('Test run finished, starting enhanced report processing'); try { // Give a moment for reports to be written await new Promise(resolve => setTimeout(resolve, 1000)); // Step 1: Discover all reports using enhanced discovery const discoveryResult = await this.discoverReports(); // Step 2: Validate and load JSON report const playwrightReport = await this.loadPlaywrightReport(discoveryResult); if (!playwrightReport) { logger_util_1.default.error('No valid Playwright JSON report found'); return; } console.log('[@alphabin/trx] Collecting metadata...'); // Step 3: Collect metadata with enhanced error handling const metadata = await this.collectMetadataWithDebugging(); // Step 4: Handle Azure upload with enhanced validation const azureUploadStatus = await this.handleEnhancedAzureUpload(discoveryResult); metadata.azureUpload = azureUploadStatus; // Step 5: Build enhanced test configuration this.buildEnhancedTestConfig(playwrightReport, metadata); // Step 6: Attach metadata to the Playwright report const reportWithMetadata = { ...playwrightReport, metadata: this.formatMetadata(metadata) }; console.log('[@alphabin/trx] Final report metadata:', JSON.stringify(reportWithMetadata.metadata, null, 2)); // Step 7: Send report to TRX server logger_util_1.default.info('Sending test results to TRX server...'); const response = await this.apiClient.sendReport(reportWithMetadata); // Step 8: Show enhanced results await this.showEnhancedResults(response, azureUploadStatus, discoveryResult); } catch (error) { logger_util_1.default.error('Error processing test results', error); const userError = error_handler_util_1.ErrorHandler.formatErrorForUser(error, 'Test result processing'); console.error(console_formatter_util_1.ConsoleFormatter.format(console_formatter_util_1.ConsoleFormatter.formatError(userError))); } } /** * Enhanced report discovery with comprehensive validation */ async discoverReports() { return await error_handler_util_1.ErrorHandler.safeExecute(async () => { console.log('[@alphabin/trx] Starting enhanced report discovery'); const result = await this.reportDiscoveryService.discoverAllReports(); // Log discovery results console.log('[@alphabin/trx] Discovery results:', { htmlFound: result.html.found, htmlPath: result.html.path, jsonFound: result.json.found, jsonPath: result.json.path, blobFound: result.blob.found, blobPath: result.blob.path }); // Validate critical findings if (!result.json.found) { console.warn('[@alphabin/trx] ⚠️ No JSON report found - this may indicate Playwright test execution issues'); } if (result.config.hasHtmlReporter && !result.html.found) { const warning = 'HTML reporter is configured but no HTML report was found'; const suggestion = 'Check if tests completed successfully and HTML reporter ran'; console.warn(console_formatter_util_1.ConsoleFormatter.format(console_formatter_util_1.ConsoleFormatter.formatWarning(warning, suggestion))); } return result; }, { operation: 'Report discovery', recoverable: true }, { html: { type: 'html', found: false }, json: { type: 'json', found: false }, blob: { type: 'blob', found: false }, config: { hasHtmlReporter: false, hasJsonReporter: false, hasBlobReporter: false, reporters: [] } }) || { html: { type: 'html', found: false }, json: { type: 'json', found: false }, blob: { type: 'blob', found: false }, config: { hasHtmlReporter: false, hasJsonReporter: false, hasBlobReporter: false, reporters: [] } }; } /** * Load and validate Playwright JSON report */ async loadPlaywrightReport(discoveryResult) { return await error_handler_util_1.ErrorHandler.safeExecute(async () => { if (!discoveryResult.json.found || !discoveryResult.json.path) { logger_util_1.default.debug('No JSON report path available'); return null; } const reportPath = discoveryResult.json.path; logger_util_1.default.debug(`Loading Playwright report from: ${reportPath}`); if (!fs_1.default.existsSync(reportPath)) { logger_util_1.default.error(`Report file not found: ${reportPath}`); return null; } const report = (0, fileSystem_util_1.readJsonFile)(reportPath); if (!report) { logger_util_1.default.error('Failed to parse JSON report file'); return null; } // Basic validation if (!report.config || !report.suites) { logger_util_1.default.error('Invalid report structure - missing config or suites'); return null; } logger_util_1.default.debug('Playwright report loaded successfully'); return report; }, { operation: 'Load Playwright report', recoverable: false }, null); } /** * Enhanced Azure upload with better validation and error handling */ async handleEnhancedAzureUpload(discoveryResult) { return await error_handler_util_1.ErrorHandler.safeExecute(async () => { const uploadStatus = { status: 'disabled' }; // Check if upload is disabled if (!this.config.isUploadEnabled()) { logger_util_1.default.debug('Azure upload is disabled'); return uploadStatus; } // Check if HTML report files were found (prioritize actual files over config) if (!discoveryResult.html.found || !discoveryResult.html.files) { // Only show config warning if no files found if (!discoveryResult.config.hasHtmlReporter) { const warning = 'HTML reporter not configured in Playwright config and no HTML files found'; const suggestion = 'Add html reporter to your Playwright config or set disableUpload: true'; console.warn(console_formatter_util_1.ConsoleFormatter.format(console_formatter_util_1.ConsoleFormatter.formatWarning(warning, suggestion))); } else { const warning = 'HTML reporter is configured but no HTML report files were found'; const suggestion = 'Ensure tests completed successfully and HTML report was generated'; console.warn(console_formatter_util_1.ConsoleFormatter.format(console_formatter_util_1.ConsoleFormatter.formatWarning(warning, suggestion))); } uploadStatus.status = 'not-found'; return uploadStatus; } // If we found HTML files, proceed regardless of config parsing results if (discoveryResult.html.found && discoveryResult.html.files) { if (!discoveryResult.config.hasHtmlReporter) { console.log('[@alphabin/trx] HTML files found despite config parsing issues - proceeding with upload'); } } logger_util_1.default.debug(`Found HTML report with ${discoveryResult.html.files.length} files for upload`); // Request SAS token with retry const sasTokenResponse = await error_handler_util_1.ErrorHandler.withRetry(() => this.azureStorageService.requestSasToken(), { operation: 'SAS token request' }); if (!sasTokenResponse || !sasTokenResponse.success) { throw new Error('Failed to obtain SAS token for Azure upload'); } // Validate and filter files const allowedTypes = sasTokenResponse.data.uploadInstructions.allowedFileTypes; const validFiles = this.reportDiscoveryService.validateFiles(discoveryResult.html.files, allowedTypes); if (validFiles.length === 0) { logger_util_1.default.warn('No valid files found for upload after filtering'); uploadStatus.status = 'not-found'; return uploadStatus; } // Check total size limit const maxSize = sasTokenResponse.data.maxSize; const totalSize = this.reportDiscoveryService.getTotalSize(validFiles); let filesToUpload = validFiles; if (totalSize > maxSize) { const sizeWarning = `Total file size (${Math.round(totalSize / 1024 / 1024)}MB) exceeds limit (${Math.round(maxSize / 1024 / 1024)}MB)`; const sizeSuggestion = 'Prioritizing HTML files and smaller assets'; console.warn(console_formatter_util_1.ConsoleFormatter.format(console_formatter_util_1.ConsoleFormatter.formatWarning(sizeWarning, sizeSuggestion))); filesToUpload = this.reportDiscoveryService.filterFilesBySize(validFiles, maxSize); } console.log(console_formatter_util_1.ConsoleFormatter.format(console_formatter_util_1.ConsoleFormatter.formatProgress(`[@alphabin/trx] Uploading ${filesToUpload.length} files to Azure Blob Storage...`))); // Upload files with retry const uploadResults = await error_handler_util_1.ErrorHandler.withRetry(() => this.azureStorageService.uploadFiles(filesToUpload, sasTokenResponse.data), { operation: 'File upload to Azure' }, { maxAttempts: 2 } // Reduce retries for bulk upload ); // Check results const successfulUploads = uploadResults.filter(r => r.success); const failedUploads = uploadResults.filter(r => !r.success); if (successfulUploads.length === 0) { throw new Error('All file uploads failed'); } if (failedUploads.length > 0) { const failureWarning = `${failedUploads.length} of ${uploadResults.length} files failed to upload`; console.warn(console_formatter_util_1.ConsoleFormatter.format(console_formatter_util_1.ConsoleFormatter.formatWarning(failureWarning))); } // Construct the final URL to index.html const indexHtmlUrl = `${sasTokenResponse.data.containerUrl}/${sasTokenResponse.data.blobPath}/index.html`; uploadStatus.status = 'uploaded'; uploadStatus.url = indexHtmlUrl; logger_util_1.default.debug(`Azure upload completed successfully. URL: ${indexHtmlUrl}`); return uploadStatus; }, { operation: 'Azure upload process', recoverable: true }, { status: 'failed' }) || { status: 'failed' }; } /** * Build enhanced test configuration with better browser detection */ buildEnhancedTestConfig(report, metadata) { try { const cfg = report.config; // Enhanced browser parameter detection const browsers = []; const runOptions = this.config.get(); // Default viewport & headless from environment const defaultViewport = `${process.env.VIEWPORT_WIDTH || '1280'}x${process.env.VIEWPORT_HEIGHT || '720'}`; const defaultHeadless = (process.env.HEADLESS || process.env.PLAYWRIGHT_HEADLESS) !== 'false'; // Global run settings const actualWorkers = cfg.metadata?.actualWorkers ?? cfg.workers ?? 0; const timeout = cfg.timeout ?? 0; const preserveOutput = cfg.preserveOutput ?? ''; const fullyParallel = cfg.fullyParallel ?? false; const forbidOnly = cfg.forbidOnly ?? false; const projectsCount = Array.isArray(cfg.projects) ? cfg.projects.length : 0; const shard = cfg.shard ?? null; // Enhanced reporters array const reportersArray = Array.isArray(cfg.reporter) ? cfg.reporter.map((r) => Array.isArray(r) ? { name: r[0], options: r[1] } : { name: r }) : []; // Load full playwright config for enhanced browser detection let pwConfig = {}; if (this.fullConfig.configFile) { try { pwConfig = require(this.fullConfig.configFile); } catch (error) { logger_util_1.default.debug('Could not load Playwright config file for browser detection', error); } } const globalUse = pwConfig.use || {}; const pwProjects = Array.isArray(pwConfig.projects) ? pwConfig.projects : []; // Build enhanced browsers array if (Array.isArray(cfg.projects)) { for (const reportProj of cfg.projects) { const browserId = reportProj.id ?? ''; const name = reportProj.name ?? ''; const version = metadata.system.playwright; // Find use overrides with better matching const configProj = pwProjects.find((p) => p.id === browserId || p.name === name || p.name === reportProj.name) || {}; const projUse = configProj.use || {}; // Enhanced viewport detection let viewportSize; if (projUse.viewport && typeof projUse.viewport.width === 'number' && typeof projUse.viewport.height === 'number') { viewportSize = `${projUse.viewport.width}x${projUse.viewport.height}`; } else if (globalUse.viewport && typeof globalUse.viewport.width === 'number' && typeof globalUse.viewport.height === 'number') { viewportSize = `${globalUse.viewport.width}x${globalUse.viewport.height}`; } else { viewportSize = defaultViewport; } // Enhanced headless detection let headlessMode; if (projUse.headless !== undefined) { headlessMode = projUse.headless; } else if (globalUse.headless !== undefined) { headlessMode = globalUse.headless; } else { headlessMode = defaultHeadless; } browsers.push({ browserId, name, version, viewport: viewportSize, headless: headlessMode, repeatEach: reportProj.repeatEach ?? 0, retries: reportProj.retries ?? 0, testDir: reportProj.testDir ?? '', outputDir: reportProj.outputDir ?? '' }); } } // Build enhanced test metadata metadata.test = { config: { browsers, actualWorkers, timeout, preserveOutput, fullyParallel, forbidOnly, projects: projectsCount, shard, reporters: reportersArray, grep: cfg.grep, grepInvert: cfg.grepInvert, }, customTags: runOptions.tags ?? [] }; logger_util_1.default.debug('Enhanced test configuration built successfully', { browsersCount: browsers.length, workersCount: actualWorkers, projectsCount }); } catch (error) { logger_util_1.default.error('Error building enhanced test configuration', error); // Set minimal config on error metadata.test = { config: { browsers: [], actualWorkers: 0, timeout: 0, preserveOutput: '', fullyParallel: false, forbidOnly: false, projects: 0, shard: null, reporters: [], }, customTags: [] }; } } /** * Enhanced results display with comprehensive information */ async showEnhancedResults(serverResponse, azureUploadStatus, discoveryResult) { const serverSuccess = serverResponse && serverResponse.success; const testRunId = serverResponse?.data?.testRunId; const serverUrl = this.config.get().serverUrl; // Get file count if uploaded let fileCount; if (azureUploadStatus.status === 'uploaded' && discoveryResult.html.files) { fileCount = discoveryResult.html.files.length; } // Enhanced results summary const resultsSummary = console_formatter_util_1.ConsoleFormatter.formatResultsSummary(serverSuccess, testRunId, serverUrl, azureUploadStatus, fileCount); console.log(console_formatter_util_1.ConsoleFormatter.format(resultsSummary)); // Enhanced error messages and suggestions if (!serverSuccess) { const suggestion = 'Check your server URL and API key configuration'; console.error(console_formatter_util_1.ConsoleFormatter.format(console_formatter_util_1.ConsoleFormatter.formatWarning('Server communication failed', suggestion))); } if (azureUploadStatus.status === 'failed') { const suggestion = 'Check your network connection and try again'; console.warn(console_formatter_util_1.ConsoleFormatter.format(console_formatter_util_1.ConsoleFormatter.formatWarning('Some files may not have uploaded', suggestion))); } // Additional context information if (discoveryResult.config.hasHtmlReporter && !discoveryResult.html.found) { const contextInfo = 'HTML reporter was configured but no report files were generated'; const contextSuggestion = 'This may indicate test execution issues or incorrect output paths'; console.info(console_formatter_util_1.ConsoleFormatter.format(console_formatter_util_1.ConsoleFormatter.formatInfo(`Context: ${contextInfo}. ${contextSuggestion}`))); } logger_util_1.default.debug('Enhanced results display completed'); } /** * Enhanced metadata collection with individual collector debugging */ async collectMetadataWithDebugging() { const configOptions = this.config.get(); // Initialize with defaults to ensure valid structure const metadata = { git: { branch: 'unknown', commit: { hash: 'unknown', message: '', author: '', email: '', timestamp: '' }, repository: { name: 'unknown', url: '' }, pr: { id: '', title: '', url: '' } }, ci: { provider: 'unknown', pipeline: { id: 'unknown', name: 'CI Pipeline', url: '' }, build: { number: 'unknown', trigger: '' }, environment: { name: 'local', type: '', os: 'unknown', node: 'unknown' } }, system: { hostname: 'unknown', cpu: { count: 0, model: 'unknown' }, memory: { total: 'unknown' }, os: 'unknown', nodejs: 'unknown', playwright: 'unknown' }, test: { config: { browsers: [], actualWorkers: 0, timeout: 0, preserveOutput: '', fullyParallel: false, forbidOnly: false, projects: 0, shard: null, reporters: [], }, customTags: [] } }; // Collect Git metadata if enabled if (configOptions.collectGitMetadata) { try { console.log('[@alphabin/trx] Collecting Git metadata...'); const { GitCollector } = await Promise.resolve().then(() => __importStar(require('./collectors'))); const gitCollector = new GitCollector(process.cwd()); const gitData = await gitCollector.collect(); console.log('[@alphabin/trx] Raw git data:', gitData); if (gitData && Object.keys(gitData).length > 0) { metadata.git = { ...metadata.git, ...gitData }; console.log('[@alphabin/trx] Git metadata collected:', { branch: metadata.git.branch, commitHash: metadata.git.commit.hash?.substring(0, 8), repository: metadata.git.repository.name }); } else { console.log('[@alphabin/trx] No Git metadata found'); } } catch (error) { console.warn('[@alphabin/trx] ⚠️ Failed to collect Git metadata:', error.message); console.warn('[@alphabin/trx] Stack trace:', error.stack); } } // Collect CI metadata if enabled if (configOptions.collectCiMetadata) { try { console.log('[@alphabin/trx] Collecting CI metadata...'); const { CICollector } = await Promise.resolve().then(() => __importStar(require('./collectors'))); const ciCollector = new CICollector(); const ciData = await ciCollector.collect(); console.log('[@alphabin/trx] Raw CI data:', ciData); if (ciData && Object.keys(ciData).length > 0) { metadata.ci = { ...metadata.ci, ...ciData }; console.log('[@alphabin/trx] CI metadata collected:', { provider: metadata.ci.provider, buildNumber: metadata.ci.build.number, environment: metadata.ci.environment.name }); } else { console.log('[@alphabin/trx] No CI metadata found (running locally)'); } } catch (error) { console.warn('[@alphabin/trx] ⚠️ Failed to collect CI metadata:', error.message); console.warn('[@alphabin/trx] Stack trace:', error.stack); } } // Collect System metadata if enabled if (configOptions.collectSystemMetadata) { try { console.log('[@alphabin/trx] Collecting System metadata...'); const { SystemCollector } = await Promise.resolve().then(() => __importStar(require('./collectors'))); const systemCollector = new SystemCollector(); const systemData = await systemCollector.collect(); console.log('[@alphabin/trx] Raw system data:', systemData); if (systemData && Object.keys(systemData).length > 0) { metadata.system = { ...metadata.system, ...systemData }; console.log('[@alphabin/trx] System metadata collected:', { hostname: metadata.system.hostname, os: metadata.system.os, nodejs: metadata.system.nodejs, playwright: metadata.system.playwright }); } else { console.log('[@alphabin/trx] No System metadata found'); } } catch (error) { console.warn('[@alphabin/trx] ⚠️ Failed to collect System metadata:', error.message); console.warn('[@alphabin/trx] Stack trace:', error.stack); } } // Add custom metadata if provided if (configOptions.customMetadata) { try { console.log('[@alphabin/trx] Adding custom metadata...'); for (const [key, value] of Object.entries(configOptions.customMetadata)) { const section = key.split('.')[0]; const field = key.split('.')[1]; if (section && field) { // Handle nested properties like 'test.customTags' metadata[section] = { ...metadata[section], [field]: value }; } else { // Handle top-level custom properties metadata[key] = value; } } } catch (error) { console.warn('[@alphabin/trx] ⚠️ Failed to add custom metadata:', error.message); } } console.log('[@alphabin/trx] Metadata collection completed'); return metadata; } /** * Formats metadata for the API */ formatMetadata(metadata) { // Convert the hierarchical metadata to a flat structure if needed // This method can be customized based on the server's expectations return metadata; } } exports.TestReportXReporter = TestReportXReporter; // Export default reporter exports.default = TestReportXReporter;