Files
BIPMaterialManager/src/main/services/erp/ErpBrowserManager.ts
Misaka 12a17eccb7 feat(logging-p0): replace console.* with logger and add error logging before ERP throws
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>
2026-04-04 12:12:41 +08:00

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