UNPKG

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
"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;