feat(automation): add browser launch capability and default to dev mode
This commit is contained in:
@@ -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':
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user