Eliminate console.* remnants in bootstrap, session-manager, migrations, and app entry so startup and login failures are captured in log files. Add log.error before all 14 throw sites in ERP services (auth, extractor, cleaner, browser manager) to ensure critical automation failures are traceable. Introduce capturePageContext utility for defensive Playwright page state capture during error logging. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
213 lines
5.0 KiB
TypeScript
213 lines
5.0 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) {
|
|
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
|