From 21d748b316277e00adac5f78f6bc72330217ba97 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sat, 22 Nov 2025 13:31:10 +0100 Subject: [PATCH] feat(automation): add browser launch capability and default to dev mode --- apps/companion/main/di-container.ts | 4 +- .../automation/BrowserDevToolsAdapter.ts | 126 +++++++++++++----- .../infrastructure/config/AutomationConfig.ts | 2 +- 3 files changed, 97 insertions(+), 35 deletions(-) diff --git a/apps/companion/main/di-container.ts b/apps/companion/main/di-container.ts index 5be7bf665..c3b061720 100644 --- a/apps/companion/main/di-container.ts +++ b/apps/companion/main/di-container.ts @@ -45,9 +45,11 @@ function createBrowserAutomationAdapter(mode: AutomationMode, logger: ILogger): switch (mode) { case 'dev': return new BrowserDevToolsAdapter({ - debuggingPort: config.devTools?.debuggingPort, + debuggingPort: config.devTools?.debuggingPort ?? 9222, browserWSEndpoint: config.devTools?.browserWSEndpoint, defaultTimeout: config.defaultTimeout, + launchBrowser: true, + startUrl: 'https://members-ng.iracing.com/web/racing/hosted/browse-sessions', }, logger.child({ adapter: 'BrowserDevTools' })); case 'production': diff --git a/packages/infrastructure/adapters/automation/BrowserDevToolsAdapter.ts b/packages/infrastructure/adapters/automation/BrowserDevToolsAdapter.ts index 9360f8780..3bb13a5b0 100644 --- a/packages/infrastructure/adapters/automation/BrowserDevToolsAdapter.ts +++ b/packages/infrastructure/adapters/automation/BrowserDevToolsAdapter.ts @@ -1,4 +1,4 @@ -import puppeteer, { Browser, Page, CDPSession } from 'puppeteer-core'; +import puppeteer, { Browser, Page } from 'puppeteer-core'; import { StepId } from '../../../packages/domain/value-objects/StepId'; import { IBrowserAutomation } from '../../../packages/application/ports/IBrowserAutomation'; import { @@ -27,6 +27,14 @@ export interface DevToolsConfig { typingDelay?: number; /** Whether to wait for network idle after navigation (default: true) */ waitForNetworkIdle?: boolean; + /** If true, launch a new browser instead of connecting to existing one */ + launchBrowser?: boolean; + /** Path to Chrome executable (optional, puppeteer will try to find it) */ + executablePath?: string; + /** Run browser in headless mode (default: false for visibility) */ + headless?: boolean; + /** URL to navigate to after launching browser */ + startUrl?: string; } /** @@ -52,10 +60,25 @@ export interface DevToolsConfig { * await adapter.connect(); * ``` */ +/** + * Internal type with all config fields required (with defaults applied) + */ +type ResolvedDevToolsConfig = { + browserWSEndpoint: string; + debuggingPort: number; + defaultTimeout: number; + typingDelay: number; + waitForNetworkIdle: boolean; + launchBrowser: boolean; + executablePath: string; + headless: boolean; + startUrl: string; +}; + export class BrowserDevToolsAdapter implements IBrowserAutomation { private browser: Browser | null = null; private page: Page | null = null; - private config: Required; + private config: ResolvedDevToolsConfig; private connected: boolean = false; private logger: ILogger; @@ -66,13 +89,18 @@ export class BrowserDevToolsAdapter implements IBrowserAutomation { defaultTimeout: config.defaultTimeout ?? 30000, typingDelay: config.typingDelay ?? 50, waitForNetworkIdle: config.waitForNetworkIdle ?? true, + launchBrowser: config.launchBrowser ?? false, + executablePath: config.executablePath ?? '', + headless: config.headless ?? false, + startUrl: config.startUrl ?? '', }; this.logger = logger ?? new NoOpLogAdapter(); } /** - * Connect to an existing browser via Chrome DevTools Protocol. - * The browser must be started with --remote-debugging-port flag. + * Connect to an existing browser via Chrome DevTools Protocol, + * or launch a new browser if launchBrowser is enabled. + * For connect mode, the browser must be started with --remote-debugging-port flag. */ async connect(): Promise { if (this.connected) { @@ -81,47 +109,79 @@ export class BrowserDevToolsAdapter implements IBrowserAutomation { } const startTime = Date.now(); - this.logger.info('Connecting to browser via CDP', { - debuggingPort: this.config.debuggingPort, - hasWsEndpoint: !!this.config.browserWSEndpoint - }); try { - if (this.config.browserWSEndpoint) { - // Connect using explicit WebSocket endpoint - this.logger.debug('Using explicit WebSocket endpoint'); - this.browser = await puppeteer.connect({ - browserWSEndpoint: this.config.browserWSEndpoint, + if (this.config.launchBrowser) { + // LAUNCH mode - start a new browser + this.logger.info('Launching new browser instance', { + headless: this.config.headless, + hasExecutablePath: !!this.config.executablePath, + startUrl: this.config.startUrl || '(none)' }); - } else { - // Connect using debugging port - need to fetch endpoint first - this.logger.debug('Fetching WebSocket endpoint from debugging port'); - const response = await fetch(`http://127.0.0.1:${this.config.debuggingPort}/json/version`); - const data = await response.json(); - const wsEndpoint = data.webSocketDebuggerUrl; - - this.browser = await puppeteer.connect({ - browserWSEndpoint: wsEndpoint, - }); - } - // Find iRacing tab or use the first available tab - const pages = await this.browser.pages(); - this.page = await this.findIRacingPage(pages) || pages[0]; - - if (!this.page) { - throw new Error('No pages found in browser'); + const launchArgs = [ + '--start-maximized', + '--no-sandbox', + ]; + + this.browser = await puppeteer.launch({ + headless: this.config.headless, + executablePath: this.config.executablePath || undefined, + args: launchArgs, + }); + + const pages = await this.browser.pages(); + this.page = pages[0] || await this.browser.newPage(); + + if (this.config.startUrl) { + this.logger.debug('Navigating to start URL', { url: this.config.startUrl }); + await this.page.goto(this.config.startUrl, { + waitUntil: this.config.waitForNetworkIdle ? 'networkidle2' : 'domcontentloaded' + }); + } + } else { + // CONNECT mode - connect to existing browser + this.logger.info('Connecting to browser via CDP', { + debuggingPort: this.config.debuggingPort, + hasWsEndpoint: !!this.config.browserWSEndpoint + }); + + if (this.config.browserWSEndpoint) { + // Connect using explicit WebSocket endpoint + this.logger.debug('Using explicit WebSocket endpoint'); + this.browser = await puppeteer.connect({ + browserWSEndpoint: this.config.browserWSEndpoint, + }); + } else { + // Connect using debugging port - need to fetch endpoint first + this.logger.debug('Fetching WebSocket endpoint from debugging port'); + const response = await fetch(`http://127.0.0.1:${this.config.debuggingPort}/json/version`); + const data = await response.json(); + const wsEndpoint = data.webSocketDebuggerUrl; + + this.browser = await puppeteer.connect({ + browserWSEndpoint: wsEndpoint, + }); + } + + // Find iRacing tab or use the first available tab + const pages = await this.browser.pages(); + this.page = await this.findIRacingPage(pages) || pages[0]; + + if (!this.page) { + throw new Error('No pages found in browser'); + } } // Set default timeout - this.page.setDefaultTimeout(this.config.defaultTimeout); + this.page!.setDefaultTimeout(this.config.defaultTimeout); this.connected = true; const durationMs = Date.now() - startTime; this.logger.info('Connected to browser successfully', { durationMs, - pageUrl: this.page.url(), - totalPages: pages.length + pageUrl: this.page!.url(), + launchMode: this.config.launchBrowser }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); diff --git a/packages/infrastructure/config/AutomationConfig.ts b/packages/infrastructure/config/AutomationConfig.ts index 400277d14..d554243fc 100644 --- a/packages/infrastructure/config/AutomationConfig.ts +++ b/packages/infrastructure/config/AutomationConfig.ts @@ -51,7 +51,7 @@ export interface AutomationEnvironmentConfig { */ export function loadAutomationConfig(): AutomationEnvironmentConfig { const modeEnv = process.env.AUTOMATION_MODE; - const mode: AutomationMode = isValidAutomationMode(modeEnv) ? modeEnv : 'mock'; + const mode: AutomationMode = isValidAutomationMode(modeEnv) ? modeEnv : 'dev'; return { mode,