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) {
|
switch (mode) {
|
||||||
case 'dev':
|
case 'dev':
|
||||||
return new BrowserDevToolsAdapter({
|
return new BrowserDevToolsAdapter({
|
||||||
debuggingPort: config.devTools?.debuggingPort,
|
debuggingPort: config.devTools?.debuggingPort ?? 9222,
|
||||||
browserWSEndpoint: config.devTools?.browserWSEndpoint,
|
browserWSEndpoint: config.devTools?.browserWSEndpoint,
|
||||||
defaultTimeout: config.defaultTimeout,
|
defaultTimeout: config.defaultTimeout,
|
||||||
|
launchBrowser: true,
|
||||||
|
startUrl: 'https://members-ng.iracing.com/web/racing/hosted/browse-sessions',
|
||||||
}, logger.child({ adapter: 'BrowserDevTools' }));
|
}, logger.child({ adapter: 'BrowserDevTools' }));
|
||||||
|
|
||||||
case 'production':
|
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 { StepId } from '../../../packages/domain/value-objects/StepId';
|
||||||
import { IBrowserAutomation } from '../../../packages/application/ports/IBrowserAutomation';
|
import { IBrowserAutomation } from '../../../packages/application/ports/IBrowserAutomation';
|
||||||
import {
|
import {
|
||||||
@@ -27,6 +27,14 @@ export interface DevToolsConfig {
|
|||||||
typingDelay?: number;
|
typingDelay?: number;
|
||||||
/** Whether to wait for network idle after navigation (default: true) */
|
/** Whether to wait for network idle after navigation (default: true) */
|
||||||
waitForNetworkIdle?: boolean;
|
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();
|
* 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 {
|
export class BrowserDevToolsAdapter implements IBrowserAutomation {
|
||||||
private browser: Browser | null = null;
|
private browser: Browser | null = null;
|
||||||
private page: Page | null = null;
|
private page: Page | null = null;
|
||||||
private config: Required<DevToolsConfig>;
|
private config: ResolvedDevToolsConfig;
|
||||||
private connected: boolean = false;
|
private connected: boolean = false;
|
||||||
private logger: ILogger;
|
private logger: ILogger;
|
||||||
|
|
||||||
@@ -66,13 +89,18 @@ export class BrowserDevToolsAdapter implements IBrowserAutomation {
|
|||||||
defaultTimeout: config.defaultTimeout ?? 30000,
|
defaultTimeout: config.defaultTimeout ?? 30000,
|
||||||
typingDelay: config.typingDelay ?? 50,
|
typingDelay: config.typingDelay ?? 50,
|
||||||
waitForNetworkIdle: config.waitForNetworkIdle ?? true,
|
waitForNetworkIdle: config.waitForNetworkIdle ?? true,
|
||||||
|
launchBrowser: config.launchBrowser ?? false,
|
||||||
|
executablePath: config.executablePath ?? '',
|
||||||
|
headless: config.headless ?? false,
|
||||||
|
startUrl: config.startUrl ?? '',
|
||||||
};
|
};
|
||||||
this.logger = logger ?? new NoOpLogAdapter();
|
this.logger = logger ?? new NoOpLogAdapter();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect to an existing browser via Chrome DevTools Protocol.
|
* Connect to an existing browser via Chrome DevTools Protocol,
|
||||||
* The browser must be started with --remote-debugging-port flag.
|
* 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> {
|
async connect(): Promise<void> {
|
||||||
if (this.connected) {
|
if (this.connected) {
|
||||||
@@ -81,47 +109,79 @@ export class BrowserDevToolsAdapter implements IBrowserAutomation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
this.logger.info('Connecting to browser via CDP', {
|
|
||||||
debuggingPort: this.config.debuggingPort,
|
|
||||||
hasWsEndpoint: !!this.config.browserWSEndpoint
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.config.browserWSEndpoint) {
|
if (this.config.launchBrowser) {
|
||||||
// Connect using explicit WebSocket endpoint
|
// LAUNCH mode - start a new browser
|
||||||
this.logger.debug('Using explicit WebSocket endpoint');
|
this.logger.info('Launching new browser instance', {
|
||||||
this.browser = await puppeteer.connect({
|
headless: this.config.headless,
|
||||||
browserWSEndpoint: this.config.browserWSEndpoint,
|
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 launchArgs = [
|
||||||
const pages = await this.browser.pages();
|
'--start-maximized',
|
||||||
this.page = await this.findIRacingPage(pages) || pages[0];
|
'--no-sandbox',
|
||||||
|
];
|
||||||
if (!this.page) {
|
|
||||||
throw new Error('No pages found in browser');
|
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
|
// Set default timeout
|
||||||
this.page.setDefaultTimeout(this.config.defaultTimeout);
|
this.page!.setDefaultTimeout(this.config.defaultTimeout);
|
||||||
|
|
||||||
this.connected = true;
|
this.connected = true;
|
||||||
const durationMs = Date.now() - startTime;
|
const durationMs = Date.now() - startTime;
|
||||||
this.logger.info('Connected to browser successfully', {
|
this.logger.info('Connected to browser successfully', {
|
||||||
durationMs,
|
durationMs,
|
||||||
pageUrl: this.page.url(),
|
pageUrl: this.page!.url(),
|
||||||
totalPages: pages.length
|
launchMode: this.config.launchBrowser
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export interface AutomationEnvironmentConfig {
|
|||||||
*/
|
*/
|
||||||
export function loadAutomationConfig(): AutomationEnvironmentConfig {
|
export function loadAutomationConfig(): AutomationEnvironmentConfig {
|
||||||
const modeEnv = process.env.AUTOMATION_MODE;
|
const modeEnv = process.env.AUTOMATION_MODE;
|
||||||
const mode: AutomationMode = isValidAutomationMode(modeEnv) ? modeEnv : 'mock';
|
const mode: AutomationMode = isValidAutomationMode(modeEnv) ? modeEnv : 'dev';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mode,
|
mode,
|
||||||
|
|||||||
Reference in New Issue
Block a user