feat(automation): add browser launch capability and default to dev mode

This commit is contained in:
2025-11-22 13:31:10 +01:00
parent bb17a4a085
commit 21d748b316
3 changed files with 97 additions and 35 deletions

View File

@@ -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':

View File

@@ -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<DevToolsConfig>;
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<void> {
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);

View File

@@ -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,