- Removed ERP_HEADLESS, ERP_IGNORE_HTTPS_ERRORS, ERP_AUTO_CLOSE_BROWSER from ConfigManager .env operations. - Hardcoded these values in ConfigManager's default settings and getAllSettings() return. - Hardcoded ignoreHTTPSErrors to true inside ErpBrowserManager when creating a new browser context. Co-authored-by: luwamgere15-crypto <255338376+luwamgere15-crypto@users.noreply.github.com>
209 lines
4.9 KiB
TypeScript
209 lines
4.9 KiB
TypeScript
/**
|
|
* 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<BrowserConfig> = {
|
|
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<BrowserConfig>
|
|
private session: BrowserSession | null = null
|
|
|
|
constructor(config?: BrowserConfig) {
|
|
this.config = { ...DEFAULT_CONFIG, ...config }
|
|
}
|
|
|
|
/**
|
|
* Launch a new browser instance
|
|
*/
|
|
async launch(): Promise<Browser> {
|
|
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<BrowserContext> {
|
|
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<Page> {
|
|
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<BrowserSession> {
|
|
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<void> {
|
|
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<void> {
|
|
const page = this.session?.page
|
|
if (!page) {
|
|
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
|