diff --git a/src/main/models/playwright.types.ts b/src/main/models/playwright.types.ts new file mode 100644 index 0000000..319edfe --- /dev/null +++ b/src/main/models/playwright.types.ts @@ -0,0 +1,17 @@ +import { Browser, BrowserContext, Page, Frame } from 'playwright'; + +export interface Credentials { + username: string; + password: string; +} + +export interface Session { + browser: Browser; + context: BrowserContext; + page: Page; + mainFrame: Frame; +} + +export interface LoginResult extends Session { + success: boolean; +} diff --git a/src/main/services/playwright.service.ts b/src/main/services/playwright.service.ts new file mode 100644 index 0000000..ed20cd7 --- /dev/null +++ b/src/main/services/playwright.service.ts @@ -0,0 +1,123 @@ +import { chromium, Browser, BrowserContext, Page, Frame } from 'playwright'; +import { Credentials, Session, LoginResult } from '../models/playwright.types'; +import { BrowserConfig, ERPConfig } from '../models/config.types'; +import { LoggerService } from './logger.service'; + +export class PlaywrightService { + private browser?: Browser; + private context?: BrowserContext; + + constructor( + private browserConfig: BrowserConfig, + private erpConfig: ERPConfig + ) {} + + async launchBrowser(): Promise { + try { + LoggerService.info('Launching Chromium browser...'); + + this.browser = await chromium.launch({ + headless: this.browserConfig.headless, + slowMo: this.browserConfig.slowMo, + timeout: this.browserConfig.timeout, + }); + + this.context = await this.browser.newContext({ + ignoreHTTPSErrors: this.erpConfig.ignoreHttpsErrors, + }); + + LoggerService.info('Browser launched successfully'); + return this.browser; + } catch (error) { + LoggerService.error('Failed to launch browser', error); + throw error; + } + } + + async login(credentials: Credentials): Promise { + if (!this.browser || !this.context) { + throw new Error('Browser not launched'); + } + + let page: Page | undefined; + + try { + LoggerService.info('Attempting ERP login...'); + + page = await this.context.newPage(); + await page.goto(this.erpConfig.baseUrl, { + timeout: this.browserConfig.timeout, + }); + + // Wait for login form + await page.waitForSelector('input[name="username"]', { + timeout: this.browserConfig.timeout, + }); + + // Fill credentials + await page.fill('input[name="username"]', credentials.username); + await page.fill('input[name="password"]', credentials.password); + + // Click login button + await page.click('button[type="submit"]'); + + // Wait for navigation + await page.waitForLoadState('domcontentloaded'); + + // Get main frame + const mainFrame = page.mainFrame(); + + LoggerService.info('Login successful'); + + return { + success: true, + browser: this.browser, + context: this.context, + page, + mainFrame, + }; + } catch (error) { + LoggerService.error('Login failed', error); + + // Clean up page on error to prevent resource leak + if (page) { + try { + await page.close(); + } catch (closeError) { + LoggerService.error('Failed to close page after login error', closeError); + } + } + + throw error; + } + } + + async closeBrowser(): Promise { + // Close context first + if (this.context) { + try { + await this.context.close(); + LoggerService.info('Browser context closed'); + } catch (error) { + LoggerService.error('Failed to close browser context', error); + } + } + + // Always attempt to close browser, even if context.close() failed + if (this.browser) { + try { + await this.browser.close(); + LoggerService.info('Browser closed'); + } catch (error) { + LoggerService.error('Failed to close browser', error); + } + } + } + + async newPage(): Promise { + if (!this.context) { + throw new Error('Browser context not initialized'); + } + return await this.context.newPage(); + } +}