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