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