/** * Browser Manager - Centralized browser lifecycle management * Provides singleton browser instances with proper cleanup and configuration */ const { chromium, firefox, webkit } = require('playwright'); const path = require('path'); class BrowserManager { constructor() { this.browser = null; this.context = null; this.page = null; this.config = null; } /** * Initialize browser with configuration * @param {Object} config - Browser configuration * @returns {Promise} */ async initialize(config) { this.config = { browserType: 'chromium', headless: true, slowMo: 0, timeout: 30000, viewport: { width: 1280, height: 720 }, video: false, screenshot: 'only-on-failure', ...config }; // Launch browser based on type const browserType = this._getBrowserType(this.config.browserType); this.browser = await browserType.launch({ headless: this.config.headless, slowMo: this.config.slowMo, args: this._getBrowserArgs() }); // Create context with configuration this.context = await this.browser.newContext({ viewport: this.config.viewport, video: this.config.video ? { dir: path.join(process.cwd(), 'tests/evidence/videos') } : undefined, recordVideo: this.config.video ? { dir: path.join(process.cwd(), 'tests/evidence/videos'), size: this.config.viewport } : undefined }); // Set default timeout this.context.setDefaultTimeout(this.config.timeout); // Create initial page this.page = await this.context.newPage(); // Set up page defaults await this._setupPageDefaults(); return { browser: this.browser, context: this.context, page: this.page }; } /** * Get new page instance * @returns {Promise} */ async getNewPage() { if (!this.context) { throw new Error('Browser not initialized. Call initialize() first.'); } return await this.context.newPage(); } /** * Get current page * @returns {Page} */ getCurrentPage() { if (!this.page) { throw new Error('No active page. Browser may not be initialized.'); } return this.page; } /** * Close current page * @returns {Promise} */ async closePage() { if (this.page) { await this.page.close(); this.page = null; } } /** * Clean shutdown of browser * @returns {Promise} */ async cleanup() { try { if (this.page) { await this.page.close(); this.page = null; } if (this.context) { await this.context.close(); this.context = null; } if (this.browser) { await this.browser.close(); this.browser = null; } } catch (error) { console.warn('Error during browser cleanup:', error.message); } } /** * Take screenshot of current page * @param {string} filename - Screenshot filename * @param {Object} options - Screenshot options * @returns {Promise} Path to screenshot */ async takeScreenshot(filename, options = {}) { if (!this.page) { throw new Error('No active page for screenshot'); } const screenshotPath = path.join( process.cwd(), 'tests/evidence/screenshots', filename ); await this.page.screenshot({ path: screenshotPath, fullPage: true, ...options }); return screenshotPath; } /** * Wait for page load with network idle * @param {number} timeout - Wait timeout * @returns {Promise} */ async waitForPageLoad(timeout = 30000) { if (!this.page) return; try { await this.page.waitForLoadState('networkidle', { timeout }); } catch (error) { console.warn('Page load wait timeout:', error.message); } } /** * Get browser type instance * @param {string} browserType - Browser type name * @returns {BrowserType} * @private */ _getBrowserType(browserType) { switch (browserType.toLowerCase()) { case 'firefox': return firefox; case 'webkit': case 'safari': return webkit; case 'chromium': case 'chrome': default: return chromium; } } /** * Get browser launch arguments * @returns {string[]} * @private */ _getBrowserArgs() { return [ '--no-sandbox', '--disable-dev-shm-usage', '--disable-extensions', '--disable-background-timer-throttling', '--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding', '--disable-features=TranslateUI', '--no-default-browser-check', '--no-first-run' ]; } /** * Set up default page configuration * @returns {Promise} * @private */ async _setupPageDefaults() { if (!this.page) return; // Set user agent await this.page.setExtraHTTPHeaders({ 'User-Agent': 'HVAC-Testing-Framework/2.0' }); // Set up console logging this.page.on('console', (msg) => { if (msg.type() === 'error') { console.error('Browser console error:', msg.text()); } }); // Set up page error handling this.page.on('pageerror', (error) => { console.error('Page error:', error.message); }); // Set up network error handling this.page.on('requestfailed', (request) => { console.warn('Request failed:', request.url(), request.failure()?.errorText); }); } } // Singleton instance let browserManager = null; /** * Get singleton BrowserManager instance * @returns {BrowserManager} */ function getBrowserManager() { if (!browserManager) { browserManager = new BrowserManager(); } return browserManager; } module.exports = { BrowserManager, getBrowserManager };