@alphabin/trx
Version:
TRX reporter for Playwright tests with Azure Blob Storage upload support
784 lines (782 loc) • 33.1 kB
JavaScript
"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;