UNPKG

@alphabin/trx

Version:

TRX reporter for Playwright tests with Azure Blob Storage upload support

784 lines (782 loc) 33.1 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.PlaywrightConfigParser = void 0; const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const logger_util_1 = __importDefault(require("./logger.util")); /** * Enhanced utility for parsing Playwright configuration files with static analysis */ class PlaywrightConfigParser { constructor(rootDir = process.cwd()) { this.rootDir = rootDir; } /** * Analyzes Playwright configuration with enhanced parsing */ async analyzeConfig() { const result = { hasHtmlReporter: false, hasJsonReporter: false, hasBlobReporter: false, reporters: [] }; try { const configPath = this.findConfigFile(); if (!configPath) { logger_util_1.default.debug('No Playwright config file found'); return result; } result.configPath = configPath; // Use console.log for immediate debug output if (process.env.DEBUG && process.env.DEBUG.includes('alphabin:trx')) { console.log(`[@alphabin/trx] Parsing Playwright config: ${configPath}`); } logger_util_1.default.debug(`Parsing Playwright config: ${configPath}`); // Try multiple approaches to load the config let parsedConfig = null; // Method 1: Try requiring with ts-node if available try { // For TypeScript configs, try to register ts-node if available if (configPath.endsWith('.ts')) { try { require('ts-node/register'); } catch { // ts-node not available, continue anyway } } // Clear require cache to get fresh config delete require.cache[require.resolve(configPath)]; const configModule = require(configPath); parsedConfig = configModule.default || configModule; if (process.env.DEBUG && process.env.DEBUG.includes('alphabin:trx')) { console.log('[@alphabin/trx] Successfully loaded config via require()'); } logger_util_1.default.debug('Successfully loaded config via require()'); } catch (requireError) { logger_util_1.default.debug('Dynamic require failed, trying alternative methods', requireError); // Method 2: Try executing the config using node with typescript compilation try { parsedConfig = await this.executeTypescriptConfig(configPath); if (parsedConfig) { logger_util_1.default.debug('Successfully loaded config via TypeScript execution'); } } catch (tsError) { logger_util_1.default.debug('TypeScript execution failed, trying static parsing', tsError); // Method 3: Fallback to static parsing const configContent = fs_1.default.readFileSync(configPath, 'utf-8'); parsedConfig = this.parseConfigContent(configContent); if (parsedConfig) { logger_util_1.default.debug('Successfully loaded config via static parsing'); } } } if (!parsedConfig) { logger_util_1.default.debug('Failed to parse config content'); return result; } if (process.env.DEBUG && process.env.DEBUG.includes('alphabin:trx')) { console.log('[@alphabin/trx] Raw config.reporter:', parsedConfig.reporter); } logger_util_1.default.debug('Raw config.reporter:', parsedConfig.reporter); // Extract reporter configurations result.reporters = this.extractReporterConfigs(parsedConfig.reporter); if (process.env.DEBUG && process.env.DEBUG.includes('alphabin:trx')) { console.log('[@alphabin/trx] Extracted reporters:', result.reporters); } logger_util_1.default.debug('Extracted reporters:', result.reporters); // Analyze each reporter for (const reporter of result.reporters) { if (process.env.DEBUG && process.env.DEBUG.includes('alphabin:trx')) { console.log(`[@alphabin/trx] Processing reporter: ${reporter.name}`, reporter.options); } logger_util_1.default.debug(`Processing reporter: ${reporter.name}`, reporter.options); switch (reporter.name) { case 'html': result.hasHtmlReporter = true; // Handle both outputFolder and outputDir for HTML reporter const htmlOutput = reporter.options?.outputDir || reporter.options?.outputFolder || 'playwright-report'; result.htmlOutputDir = this.resolveOutputPath(htmlOutput); logger_util_1.default.debug(`HTML reporter config - outputDir: ${htmlOutput}, resolved: ${result.htmlOutputDir}`); break; case 'json': result.hasJsonReporter = true; const jsonOutput = reporter.options?.outputFile || 'test-results/results.json'; result.jsonOutputFile = this.resolveOutputPath(jsonOutput); logger_util_1.default.debug(`JSON reporter config - outputFile: ${jsonOutput}, resolved: ${result.jsonOutputFile}`); break; case 'blob': result.hasBlobReporter = true; const blobOutput = reporter.options?.outputDir || 'blob-report'; result.blobOutputDir = this.resolveOutputPath(blobOutput); logger_util_1.default.debug(`Blob reporter config - outputDir: ${blobOutput}, resolved: ${result.blobOutputDir}`); break; } } // Extract global output directory if (parsedConfig.use?.outputDir) { result.globalOutputDir = this.resolveOutputPath(parsedConfig.use.outputDir); logger_util_1.default.debug(`Global outputDir: ${result.globalOutputDir}`); } // Set defaults if not specified if (result.hasHtmlReporter && !result.htmlOutputDir) { result.htmlOutputDir = this.resolveOutputPath('playwright-report'); logger_util_1.default.debug(`Using default HTML output: ${result.htmlOutputDir}`); } if (result.hasJsonReporter && !result.jsonOutputFile) { result.jsonOutputFile = this.resolveOutputPath('test-results/results.json'); logger_util_1.default.debug(`Using default JSON output: ${result.jsonOutputFile}`); } logger_util_1.default.debug('Final config analysis result:', { hasHtmlReporter: result.hasHtmlReporter, hasJsonReporter: result.hasJsonReporter, hasBlobReporter: result.hasBlobReporter, htmlOutputDir: result.htmlOutputDir, jsonOutputFile: result.jsonOutputFile, reportersCount: result.reporters.length }); return result; } catch (error) { logger_util_1.default.error('Error in config analysis', error); return result; } } /** * Executes TypeScript config using child process */ async executeTypescriptConfig(configPath) { try { const { execCommand } = await Promise.resolve().then(() => __importStar(require('./exec.util'))); // Create a temporary script to extract config const extractScript = ` const { spawn } = require('child_process'); const path = require('path'); // Try to load the config let config; try { delete require.cache[require.resolve('${configPath}')]; const configModule = require('${configPath}'); config = configModule.default || configModule; console.log(JSON.stringify(config, null, 2)); } catch (error) { console.error('Error loading config:', error.message); process.exit(1); } `; const result = await execCommand(`node -e "${extractScript.replace(/"/g, '\\"')}"`); return JSON.parse(result); } catch (error) { logger_util_1.default.debug('TypeScript execution failed', error); return null; } } /** * Enhanced static parsing for complex TypeScript configs */ parseConfigContent(content) { try { // Enhanced cleaning for TypeScript configs let cleanContent = content .replace(/import\s+.*?from\s+['"][^'"]*['"];?\s*/g, '') // Remove imports .replace(/import\s+\*\s+as\s+\w+\s+from\s+['"][^'"]*['"];?\s*/g, '') // Remove namespace imports .replace(/export\s+default\s+/, 'module.exports = ') // Convert export to module.exports .replace(/defineConfig\s*\(\s*/, '') // Remove defineConfig wrapper .replace(/\)\s*;?\s*$/, '') // Remove closing parenthesis .replace(/\/\*[\s\S]*?\*\//g, '') // Remove block comments .replace(/\/\/.*$/gm, '') // Remove line comments .replace(/const\s+\w+\s*=\s*[^;]+;/g, '') // Remove const declarations .replace(/function\s+\w+\s*\([^)]*\)\s*\{[^}]*\}/g, '') // Remove function declarations .replace(/\w+\(\);/g, ''); // Remove function calls // Handle environment variables more robustly cleanContent = cleanContent.replace(/process\.env\[['"]([^'"]+)['"]\]/g, (match, envVar) => { return `"${process.env[envVar] || ''}"`; }); cleanContent = cleanContent.replace(/process\.env\.(\w+)/g, (match, envVar) => { return `"${process.env[envVar] || ''}"`; }); // Create a more comprehensive sandbox const sandbox = { process: { env: process.env, argv: process.argv, cwd: () => this.rootDir }, __dirname: path_1.default.dirname(this.rootDir), __filename: '', require: () => ({}), devices: { 'Desktop Chrome': { viewport: { width: 1280, height: 720 } } }, module: { exports: {} }, exports: {}, console: { log: () => { }, error: () => { } }, Buffer: Buffer, undefined: undefined, null: null, true: true, false: false }; // Try to extract just the reporter configuration using regex if full parsing fails const reporterMatch = cleanContent.match(/reporter\s*:\s*(\[[^\]]*\])/s); if (reporterMatch) { const reporterString = reporterMatch[1]; const reporters = this.parseReporterArrayAdvanced(reporterString); return { reporter: reporters }; } // Fallback: try to execute with Function constructor const wrappedContent = ` (function() { try { ${cleanContent} } catch (e) { return null; } })() `; const configFunction = new Function(...Object.keys(sandbox), `return ${wrappedContent}`); const result = configFunction(...Object.values(sandbox)); return result || sandbox.module.exports; } catch (error) { logger_util_1.default.debug('Enhanced static parsing failed, trying regex extraction', error); return this.extractConfigUsingRegex(content); } } /** * Advanced reporter array parsing with better TypeScript support */ parseReporterArrayAdvanced(reporterString) { const reporters = []; try { // Remove whitespace and normalize const normalized = reporterString.replace(/\s+/g, ' ').trim(); // Enhanced patterns for TypeScript configs const patterns = [ // ['reporter', { option: 'value', option2: 'value2' }] /\['([^']+)',\s*\{\s*([^}]*)\s*\}\s*\]/g, // ['reporter'] /\['([^']+)'\]/g, // 'reporter' /'([^']+)'/g ]; let foundReporters = false; for (const pattern of patterns) { let match; while ((match = pattern.exec(normalized)) !== null) { foundReporters = true; const reporterName = match[1]; const optionsString = match[2]; const options = {}; if (optionsString) { // Enhanced option parsing const optionPatterns = [ /(\w+)\s*:\s*'([^']*)'/g, // 'string values' /(\w+)\s*:\s*"([^"]*)"/g, // "string values" /(\w+)\s*:\s*`([^`]*)`/g, // `template strings` /(\w+)\s*:\s*(true|false)/g, // boolean values /(\w+)\s*:\s*(\d+)/g, // numeric values /(\w+)\s*:\s*(\w+)/g // other values ]; for (const optPattern of optionPatterns) { let optionMatch; while ((optionMatch = optPattern.exec(optionsString)) !== null) { const key = optionMatch[1]; let value = optionMatch[2]; // Type conversion if (value === 'true') value = true; else if (value === 'false') value = false; else if (/^\d+$/.test(value)) value = parseInt(value); options[key] = value; } } } reporters.push(Object.keys(options).length > 0 ? [reporterName, options] : reporterName); } if (foundReporters) break; // Stop after first successful pattern } logger_util_1.default.debug(`Parsed ${reporters.length} reporters from config`); return reporters; } catch (error) { logger_util_1.default.debug('Advanced reporter parsing failed', error); return []; } } /** * Fallback method using regex extraction for config parsing */ extractConfigUsingRegex(content) { const config = {}; try { // Try to extract the entire reporter array more aggressively const reporterMatches = [ // Multi-line reporter array /reporter\s*:\s*\[\s*([\s\S]*?)\s*\]/, // Single-line reporter array /reporter\s*:\s*(\[[^\]]*\])/ ]; let reporterContent = null; for (const pattern of reporterMatches) { const match = content.match(pattern); if (match) { reporterContent = match[1]; break; } } if (reporterContent) { logger_util_1.default.debug('Found reporter content via regex:', reporterContent); // Parse the reporter content config.reporter = this.parseReporterFromRawContent(reporterContent); if (config.reporter.length > 0) { logger_util_1.default.debug('Successfully extracted reporters via regex:', config.reporter); } } // Extract use configuration const useMatch = content.match(/use\s*:\s*\{([^}]*)\}/s); if (useMatch) { const useContent = useMatch[1]; const outputDirMatch = useContent.match(/outputDir\s*:\s*['"`]([^'"`]+)['"`]/); if (outputDirMatch) { config.use = { outputDir: outputDirMatch[1] }; } } return config; } catch (error) { logger_util_1.default.debug('Regex extraction failed', error); return {}; } } /** * Parse reporter configuration from raw content string */ parseReporterFromRawContent(content) { const reporters = []; try { // Clean up the content const cleaned = content .replace(/\/\/.*$/gm, '') // Remove comments .replace(/\s+/g, ' ') // Normalize whitespace .trim(); logger_util_1.default.debug('Parsing reporter content:', cleaned); // Split by commas that are not inside brackets or quotes const reporterStrings = this.splitReporterArray(cleaned); for (const reporterStr of reporterStrings) { const trimmed = reporterStr.trim(); if (!trimmed) continue; logger_util_1.default.debug('Processing reporter string:', trimmed); // Parse individual reporter if (trimmed.startsWith('[')) { // Array format: ['name', { options }] or ['name'] const arrayMatch = trimmed.match(/\['([^']+)'(?:,\s*(\{[^}]*\}))?\]/); if (arrayMatch) { const name = arrayMatch[1]; const optionsStr = arrayMatch[2]; if (optionsStr) { const options = this.parseOptionsObject(optionsStr); reporters.push([name, options]); logger_util_1.default.debug(`Parsed reporter: ${name} with options:`, options); } else { reporters.push(name); logger_util_1.default.debug(`Parsed simple reporter: ${name}`); } } } else if (trimmed.startsWith('\'') || trimmed.startsWith('"')) { // Simple string format: 'name' const nameMatch = trimmed.match(/['"]([^'"]+)['"]/); if (nameMatch) { const name = nameMatch[1]; reporters.push(name); logger_util_1.default.debug(`Parsed string reporter: ${name}`); } } } return reporters; } catch (error) { logger_util_1.default.debug('Error parsing reporter from raw content', error); return []; } } /** * Split reporter array content while respecting nested structures */ splitReporterArray(content) { const parts = []; let current = ''; let depth = 0; let inQuotes = false; let quoteChar = ''; for (let i = 0; i < content.length; i++) { const char = content[i]; const prevChar = i > 0 ? content[i - 1] : ''; if ((char === '"' || char === '\'' || char === '`') && prevChar !== '\\') { if (!inQuotes) { inQuotes = true; quoteChar = char; } else if (char === quoteChar) { inQuotes = false; quoteChar = ''; } } if (!inQuotes) { if (char === '[' || char === '{') { depth++; } else if (char === ']' || char === '}') { depth--; } else if (char === ',' && depth === 0) { parts.push(current.trim()); current = ''; continue; } } current += char; } if (current.trim()) { parts.push(current.trim()); } return parts; } /** * Parse options object from string representation */ parseOptionsObject(optionsStr) { const options = {}; try { // Remove braces const content = optionsStr.replace(/^\{|\}$/g, '').trim(); // Split by commas not inside quotes const parts = this.splitOptionsString(content); for (const part of parts) { const trimmed = part.trim(); if (!trimmed) continue; // Match key: value pairs const match = trimmed.match(/(\w+)\s*:\s*(.+)/); if (match) { const key = match[1]; let value = match[2].trim(); // Remove quotes and parse value if ((value.startsWith('\'') && value.endsWith('\'')) || (value.startsWith('"') && value.endsWith('"')) || (value.startsWith('`') && value.endsWith('`'))) { value = value.slice(1, -1); } else if (value === 'true') { value = 'true'; } else if (value === 'false') { value = 'false'; } else if (/^\d+$/.test(value)) { value = parseInt(value).toString(); } options[key] = value; } } } catch (error) { logger_util_1.default.debug('Error parsing options object', error); } return options; } /** * Split options string while respecting quotes */ splitOptionsString(content) { const parts = []; let current = ''; let inQuotes = false; let quoteChar = ''; for (let i = 0; i < content.length; i++) { const char = content[i]; const prevChar = i > 0 ? content[i - 1] : ''; if ((char === '"' || char === '\'' || char === '`') && prevChar !== '\\') { if (!inQuotes) { inQuotes = true; quoteChar = char; } else if (char === quoteChar) { inQuotes = false; quoteChar = ''; } } if (!inQuotes && char === ',') { parts.push(current.trim()); current = ''; continue; } current += char; } if (current.trim()) { parts.push(current.trim()); } return parts; } /** * Parses reporter array from string representation */ parseReporterArray(reporterString) { const reporters = []; try { // Enhanced regex patterns for better option parsing const reporterPattern = /\['([^']+)'(?:,\s*\{([^}]*)\})?\]/g; const stringPattern = /'([^']+)'/g; let match; // First try the complex pattern with options while ((match = reporterPattern.exec(reporterString)) !== null) { const reporterName = match[1]; const optionsString = match[2]; const options = {}; if (optionsString) { // Enhanced option parsing for multiple formats const optionPatterns = [ /(\w+)\s*:\s*['"`]([^'"`]+)['"`]/g, // string values /(\w+)\s*:\s*(\w+)/g // boolean/other values ]; for (const pattern of optionPatterns) { let optionMatch; while ((optionMatch = pattern.exec(optionsString)) !== null) { const key = optionMatch[1]; let value = optionMatch[2]; // Convert known boolean values if (value === 'true') value = 'true'; else if (value === 'false') value = 'false'; else if (value === 'never') value = 'never'; options[key] = value; } } } reporters.push(Object.keys(options).length > 0 ? [reporterName, options] : reporterName); } // Fallback for simple string reporters if (reporters.length === 0) { reporterPattern.lastIndex = 0; // Reset regex while ((match = stringPattern.exec(reporterString)) !== null) { reporters.push(match[1]); } } } catch (error) { logger_util_1.default.debug('Error parsing reporter array', error); } return reporters; } /** * Extracts reporter configurations from parsed config */ extractReporterConfigs(reporter) { const configs = []; if (!reporter) { return configs; } try { if (Array.isArray(reporter)) { for (const rep of reporter) { if (typeof rep === 'string') { configs.push({ name: rep }); } else if (Array.isArray(rep) && rep.length > 0) { configs.push({ name: rep[0], options: rep[1] || {} }); } } } else if (typeof reporter === 'string') { configs.push({ name: reporter }); } } catch (error) { logger_util_1.default.debug('Error extracting reporter configs', error); } return configs; } /** * Resolves output path with environment variable substitution */ resolveOutputPath(outputPath) { if (!outputPath) { return ''; } try { // Resolve environment variables let resolvedPath = outputPath.replace(/\$\{([^}]+)\}/g, (match, envVar) => { return process.env[envVar] || ''; }); resolvedPath = resolvedPath.replace(/\$([A-Z_][A-Z0-9_]*)/g, (match, envVar) => { return process.env[envVar] || ''; }); // Resolve relative to root directory if (!path_1.default.isAbsolute(resolvedPath)) { resolvedPath = path_1.default.resolve(this.rootDir, resolvedPath); } return resolvedPath; } catch (error) { logger_util_1.default.debug(`Error resolving path: ${outputPath}`, error); return path_1.default.resolve(this.rootDir, outputPath); } } /** * Finds Playwright configuration file */ findConfigFile() { const configFiles = [ 'playwright.config.ts', 'playwright.config.js', 'playwright.config.mjs', 'playwright.config.cjs' ]; for (const configFile of configFiles) { const fullPath = path_1.default.join(this.rootDir, configFile); if (fs_1.default.existsSync(fullPath)) { logger_util_1.default.debug(`Found Playwright config: ${fullPath}`); return fullPath; } } return null; } /** * Gets all possible report output directories */ getReportDirectories() { const directories = []; // Add common default directories directories.push(path_1.default.join(this.rootDir, 'playwright-report'), path_1.default.join(this.rootDir, 'test-results'), path_1.default.join(this.rootDir, 'blob-report'), path_1.default.join(this.rootDir, 'reports')); return directories.filter(dir => fs_1.default.existsSync(dir)); } /** * Validates if a reporter is configured correctly */ validateReporterConfig(reporterName, config) { switch (reporterName) { case 'html': return config.hasHtmlReporter && !!config.htmlOutputDir; case 'json': return config.hasJsonReporter && !!config.jsonOutputFile; case 'blob': return config.hasBlobReporter && !!config.blobOutputDir; default: return false; } } /** * Checks if traces are likely enabled */ isTracingEnabled(config) { try { // Check global use settings if (config.use?.trace) { return config.use.trace !== 'off'; } // Check project-specific settings if (config.projects && Array.isArray(config.projects)) { for (const project of config.projects) { if (project.use?.trace && project.use.trace !== 'off') { return true; } } } return false; } catch { return false; } } /** * Gets expected trace directory */ getTraceDirectory(config) { try { // Check for test-results directory (default trace location) const testResultsDir = path_1.default.join(this.rootDir, 'test-results'); if (fs_1.default.existsSync(testResultsDir)) { return testResultsDir; } // Check config outputDir if (config.use?.outputDir) { const outputDir = path_1.default.resolve(this.rootDir, config.use.outputDir); if (fs_1.default.existsSync(outputDir)) { return outputDir; } } return null; } catch { return null; } } /** * Legacy method for backward compatibility * @deprecated Use analyzeConfig() instead */ async isHtmlReporter(reporter) { if (typeof reporter === 'string') { return reporter === 'html'; } if (Array.isArray(reporter) && reporter.length > 0) { return reporter[0] === 'html'; } return false; } } exports.PlaywrightConfigParser = PlaywrightConfigParser; exports.default = PlaywrightConfigParser;