omni-dashboard-playwright-reporter
Version:
Playwright client for publishing test results to Omni Dashboard - Transform Your Test Automation with AI-Powered Insights
327 lines (326 loc) • 17.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;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const omni_reporter_config_1 = require("./types/omni-reporter-config");
const omni_playwright_client_1 = require("./omni-playwright-client");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const uuid_1 = require("uuid");
class OmniPlaywrightReporter {
constructor() {
this.queue = Promise.resolve();
this.buildId = '';
this.omniConfig = (0, omni_reporter_config_1.getOmniReporterConfig)();
this.client = new omni_playwright_client_1.OmniPlaywrightClient(this.omniConfig);
this._buildCreationFailedLogged = false;
this.pendingUploads = []; // Track all background uploads (per-worker)
this.workerId = (0, uuid_1.v4)(); // Unique ID for this worker instance
this.buildLockFile = '';
this.buildIdFile = '';
}
async enqueue(operation) {
this.queue = this.queue.then(operation);
return this.queue;
}
async onBegin(config, suite) {
// Initialize file paths for cross-worker coordination
const testResultsDir = process.env.PLAYWRIGHT_TEST_RESULTS_DIR || path.join(process.cwd(), 'test-results');
// Ensure directory exists
try {
if (!fs.existsSync(testResultsDir)) {
fs.mkdirSync(testResultsDir, { recursive: true });
}
}
catch (error) {
console.warn(`[OmniPlaywrightReporter][OnBegin] Could not create test-results directory: ${error.message}`);
}
this.buildLockFile = path.join(testResultsDir, '.omni-build-lock');
this.buildIdFile = path.join(testResultsDir, '.omni-build-id');
// MUST await to prevent duplicate builds and ensure buildId is available before tests start
// The lock mechanism (acquireLockAndCreateBuild) ensures only one worker creates the build,
// but we need to wait for it to complete so all workers have the buildId before tests execute
await this.enqueue(async () => {
// Validate configuration
if (!this.omniConfig.projectId || !this.omniConfig.apiKey || !this.omniConfig.environment) {
console.error('[OmniPlaywrightReporter][OnBegin] Missing required configuration: projectId, apiKey, or environment');
return;
}
// Try to acquire lock and create build (only one worker should succeed)
this.buildId = await this.acquireLockAndCreateBuild(config, suite);
}).catch((error) => {
console.error(`[OmniPlaywrightReporter][OnBegin] Error in build creation: ${error.message}`);
});
}
async onTestBegin(test) {
}
async onTestEnd(test, result) {
// Fire off upload in background - completely non-blocking
// The async IIFE starts immediately but doesn't block test execution
// All synchronous work (createTestCasePayload) happens inside the async function
const uploadPromise = (async () => {
try {
// Validate configuration
if (!this.omniConfig.projectId || !this.omniConfig.apiKey || !this.omniConfig.environment) {
console.error('[OmniPlaywrightReporter][OnTestEnd] Missing required configuration: projectId, apiKey, or environment');
return;
}
// Load buildId from file if not set (in case this worker didn't create it)
if (!this.buildId && this.buildIdFile) {
this.buildId = await this.loadBuildIdFromFile();
}
// Skip test upload if buildId is empty (build creation failed)
if (!this.buildId) {
// Only log warning for first test to avoid spam
if (!this._buildCreationFailedLogged) {
console.warn('[OmniPlaywrightReporter][OnTestEnd] Build creation failed - test results will not be uploaded to Omni Dashboard');
console.warn('[OmniPlaywrightReporter][OnTestEnd] Tests will continue to execute normally');
this._buildCreationFailedLogged = true;
}
return;
}
// Upload test case - this is the only API call we need to wait for to get S3 URLs
const uploadTestCaseResponse = await this.client.uploadTestCase(test, result, this.buildId);
// Validate response structure before accessing nested properties
if (!uploadTestCaseResponse?.data?.test_cases || uploadTestCaseResponse.data.test_cases.length === 0) {
console.warn(`[OmniPlaywrightReporter] Test case upload response missing test_cases data: ${test.title}`);
return; // Continue with next test instead of crashing
}
const test_case = uploadTestCaseResponse.data.test_cases[0];
// Upload screenshots and traces in background (non-blocking)
// These are part of the same upload promise, so onEnd will wait for everything
await Promise.allSettled([
this.client.uploadScreenshots(test_case),
this.client.uploadTraces(test_case)
]).then((results) => {
// Check for any failures in the results
results.forEach((result, index) => {
if (result.status === 'rejected') {
const uploadType = index === 0 ? 'screenshots' : 'traces';
console.error(`[OmniPlaywrightReporter] Error uploading ${uploadType}: ${result.reason?.message || result.reason}`);
}
});
});
// Reduced logging for performance - only log in debug mode
if (process.env.OMNI_DEBUG === 'true') {
console.log("Reaching end of uploadTestCase");
console.log(`[OmniPlaywrightReporter] Test case uploaded successfully: ${test.title}`);
}
}
catch (error) {
// Log error but don't throw - allow remaining tests to continue
console.error(`[OmniPlaywrightReporter] Test upload failed: ${test.title} - ${error.message}`);
// Don't re-throw the error to prevent crashing the entire test run
}
})();
// Track this upload promise so onEnd can wait for all uploads to complete
this.pendingUploads.push(uploadPromise);
// Function returns immediately - upload happens in background
}
async onEnd(result) {
// Wait for all pending uploads to complete before finishing
await this.enqueue(async () => {
// Validate configuration
if (!this.omniConfig.projectId || !this.omniConfig.apiKey || !this.omniConfig.environment) {
console.error('[OmniPlaywrightReporter][OnEnd] Missing required configuration: projectId, apiKey, or environment');
return;
}
// Load buildId from file (in case this worker didn't create it)
if (!this.buildId) {
this.buildId = await this.loadBuildIdFromFile();
}
// Only complete build if this worker created it (check lock file)
await this.onEndHandler(result);
});
// Wait for all background screenshot/trace uploads to complete (per-worker)
// Capture reference to avoid race conditions if new uploads are added during wait
const uploadsToWait = [...this.pendingUploads];
if (uploadsToWait.length > 0) {
console.log(`[OmniPlaywrightReporter][OnEnd] Worker ${this.workerId.substring(0, 8)}: Waiting for ${uploadsToWait.length} background upload(s) to complete...`);
await Promise.allSettled(uploadsToWait);
console.log(`[OmniPlaywrightReporter][OnEnd] Worker ${this.workerId.substring(0, 8)}: All background uploads completed`);
}
}
async acquireLockAndCreateBuild(config, suite) {
const maxRetries = 10;
const retryDelay = 100; // ms
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
// Try to create lock file (exclusive creation)
try {
fs.writeFileSync(this.buildLockFile, this.workerId, { flag: 'wx' });
// Lock acquired! This worker will create the build
console.log(`[OmniPlaywrightReporter][OnBegin] Worker ${this.workerId.substring(0, 8)}: Acquired lock, creating build...`);
const buildId = await this.onBeginHandler(config, suite);
if (buildId) {
// Write buildId to file for other workers
fs.writeFileSync(this.buildIdFile, buildId, 'utf8');
console.log(`[OmniPlaywrightReporter][OnBegin] Build ID written to file for other workers`);
}
return buildId;
}
catch (lockError) {
// Lock file exists - another worker is creating the build
if (lockError.code === 'EEXIST') {
// Wait a bit and try to read buildId
await new Promise(resolve => setTimeout(resolve, retryDelay));
const buildId = await this.loadBuildIdFromFile();
if (buildId) {
console.log(`[OmniPlaywrightReporter][OnBegin] Worker ${this.workerId.substring(0, 8)}: Using build ID from file: ${buildId}`);
return buildId;
}
// Build not created yet, retry
continue;
}
throw lockError;
}
}
catch (error) {
if (attempt === maxRetries - 1) {
console.error(`[OmniPlaywrightReporter][OnBegin] Failed to acquire lock after ${maxRetries} attempts`);
return '';
}
await new Promise(resolve => setTimeout(resolve, retryDelay));
}
}
return '';
}
async loadBuildIdFromFile() {
try {
// Use async file operations to avoid blocking - try to read directly, catch if file doesn't exist
const buildId = await fs.promises.readFile(this.buildIdFile, 'utf8');
const trimmed = buildId.trim();
if (trimmed) {
return trimmed;
}
}
catch (error) {
// File doesn't exist or other error - this is expected if build hasn't been created yet
if (error.code !== 'ENOENT') {
console.error(`[OmniPlaywrightReporter] Error reading build ID file: ${error.message}`);
}
}
return '';
}
async onBeginHandler(config, suite) {
try {
const response = await this.client.createBuild({
duration: 0,
environment: this.omniConfig.environment,
status: 'in_progress',
});
// Validate response structure before accessing nested properties
if (!response?.data?.build?.id) {
throw new Error('Invalid build creation response: missing build ID');
}
console.log('[OmniPlaywrightReporter][OnBegin] Build created with ID:', response.data.build.id);
return response.data.build.id;
}
catch (error) {
const errorMessage = error.message || String(error);
// Detect network/DNS errors and provide helpful context
if (errorMessage.includes('ENOTFOUND') || errorMessage.includes('getaddrinfo')) {
console.error('[OmniPlaywrightReporter][OnBegin] Network error: Cannot reach Omni Dashboard API');
console.error(`[OmniPlaywrightReporter][OnBegin] Base URL: ${this.omniConfig.baseUrl}`);
console.error('[OmniPlaywrightReporter][OnBegin] Please check:');
console.error(' - Network connectivity');
console.error(' - OMNI_DASHBOARD_BASE_URL environment variable');
console.error(' - DNS resolution for the API hostname');
console.error('[OmniPlaywrightReporter][OnBegin] Tests will continue without build tracking.');
}
else {
console.error('[OmniPlaywrightReporter][OnBegin] Error creating build:', errorMessage);
}
// Don't throw - allow tests to run even if build creation fails
// Return empty string as fallback to prevent further errors
return '';
}
}
async onEndHandler(result) {
try {
// Skip if buildId is not set (build creation may have failed)
if (!this.buildId) {
console.warn('[OmniPlaywrightReporter][OnEnd] Skipping build completion - no build ID available');
return;
}
// Only complete build if this worker created it (check lock file)
// If lock is missing or unreadable, assume another worker already completed and cleaned up.
let shouldCompleteBuild = false;
try {
if (fs.existsSync(this.buildLockFile)) {
const lockOwner = fs.readFileSync(this.buildLockFile, 'utf8').trim();
shouldCompleteBuild = lockOwner === this.workerId;
}
else {
shouldCompleteBuild = false; // lock missing -> skip completion
}
}
catch (error) {
console.error(`[OmniPlaywrightReporter][OnEnd] Error checking lock file: ${error.message}`);
shouldCompleteBuild = false; // on error, skip to avoid duplicate completions
}
if (!shouldCompleteBuild) {
console.log(`[OmniPlaywrightReporter][OnEnd] Worker ${this.workerId.substring(0, 8)}: Skipping build completion (not the lock owner)`);
return;
}
// Ensure duration is never negative (result.duration may be undefined or negative)
const duration = Math.max(0, Math.round(result.duration || 0));
await this.client.completeBuild({
build_id: this.buildId,
duration: duration,
status: result.status
});
console.log(`[OmniPlaywrightReporter][OnEnd] Worker ${this.workerId.substring(0, 8)}: Build completed successfully`);
// Clean up lock and buildId files
try {
if (fs.existsSync(this.buildLockFile)) {
fs.unlinkSync(this.buildLockFile);
}
if (fs.existsSync(this.buildIdFile)) {
fs.unlinkSync(this.buildIdFile);
}
}
catch (cleanupError) {
// Ignore cleanup errors
console.warn(`[OmniPlaywrightReporter][OnEnd] Warning: Could not clean up lock files: ${cleanupError.message}`);
}
}
catch (error) {
// Log error but don't throw - allow test run to complete normally
console.error('[OmniPlaywrightReporter][OnEnd] Error completing build:', error.message);
// Don't re-throw to prevent crashing the test run
}
}
}
exports.default = OmniPlaywrightReporter;