/** * ERP Browser Manager * * Manages browser lifecycle for ERP automation. * Separates browser management from authentication logic. */ import { chromium, type Browser, type BrowserContext, type Page } from 'playwright' import { createLogger } from '../logger' const log = createLogger('ErpBrowserManager') /** * Browser configuration options */ export interface BrowserConfig { headless?: boolean slowMo?: number viewport?: { width: number; height: number } ignoreHTTPSErrors?: boolean acceptDownloads?: boolean } /** * Default browser configuration */ const DEFAULT_CONFIG: Required = { headless: false, slowMo: 100, viewport: { width: 1920, height: 1080 }, ignoreHTTPSErrors: true, acceptDownloads: true } /** * Browser session containing all browser-related objects */ export interface BrowserSession { browser: Browser context: BrowserContext page: Page } /** * ErpBrowserManager class * Manages browser lifecycle independently from ERP authentication */ export class ErpBrowserManager { private config: Required private session: BrowserSession | null = null constructor(config?: BrowserConfig) { this.config = { ...DEFAULT_CONFIG, ...config } } /** * Launch a new browser instance */ async launch(): Promise { if (this.session?.browser?.isConnected()) { log.debug('Browser already running, returning existing instance') return this.session.browser } log.info('Launching browser', { headless: this.config.headless }) const browser = await chromium.launch({ headless: this.config.headless, slowMo: this.config.slowMo, args: [ '--ignore-certificate-errors', '--ignore-ssl-errors', '--ignore-certificate-errors-spki-list', '--disable-web-security' ] }) log.info('Browser launched successfully') return browser } /** * Create a new browser context */ async createContext(browser?: Browser): Promise { const browserInstance = browser || (await this.launch()) log.debug('Creating browser context') const context = await browserInstance.newContext({ acceptDownloads: this.config.acceptDownloads, viewport: this.config.viewport, ignoreHTTPSErrors: true, javaScriptEnabled: true }) log.debug('Browser context created') return context } /** * Create a new page in the context */ async createPage(context?: BrowserContext): Promise { let contextInstance: BrowserContext if (context) { contextInstance = context } else if (this.session?.context) { contextInstance = this.session.context } else { const browser = await this.launch() contextInstance = await this.createContext(browser) } log.debug('Creating new page') const page = await contextInstance.newPage() log.debug('Page created') return page } /** * Initialize a complete browser session * This creates browser, context, and page in one call */ async initialize(): Promise { if (this.session) { log.debug('Returning existing browser session') return this.session } const browser = await this.launch() const context = await this.createContext(browser) const page = await this.createPage(context) this.session = { browser, context, page } log.info('Browser session initialized') return this.session } /** * Get the current session */ getSession(): BrowserSession | null { return this.session } /** * Check if browser is running */ isRunning(): boolean { return this.session?.browser?.isConnected() ?? false } /** * Close the browser and cleanup */ async close(): Promise { if (!this.session) { log.debug('No browser session to close') return } log.info('Closing browser session') try { if (this.session.context) { await this.session.context.close() } } catch (error) { log.warn('Error closing context', { error: error instanceof Error ? error.message : String(error) }) } try { if (this.session.browser) { await this.session.browser.close() } } catch (error) { log.warn('Error closing browser', { error: error instanceof Error ? error.message : String(error) }) } this.session = null log.info('Browser session closed') } /** * Navigate to a URL */ async navigate(url: string, options?: { timeout?: number }): Promise { const page = this.session?.page if (!page) { log.error('No page available for navigation', { url, hasSession: !!this.session }) throw new Error('No page available. Call initialize() first.') } log.info('Navigating to URL', { url }) await page.goto(url, { timeout: options?.timeout ?? 30000 }) await page.waitForLoadState('domcontentloaded', { timeout: options?.timeout ?? 10000 }) log.debug('Page loaded') } } export default ErpBrowserManager