This commit is contained in:
2025-12-04 15:15:24 +01:00
parent b7d5551ea7
commit c698a0b893
119 changed files with 1167 additions and 2652 deletions

View File

@@ -1,4 +1,4 @@
import { AutomationEvent } from '../../application/ports/IAutomationEventPublisher';
import { AutomationEvent } from '@gridpilot/automation/application/ports/AutomationEventPublisherPort';
export type LifecycleCallback = (event: AutomationEvent) => Promise<void> | void;

View File

@@ -1,7 +1,7 @@
import { Result } from '../../../shared/result/Result';
import { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice';
import { CheckoutState } from '@gridpilot/automation/domain/value-objects/CheckoutState';
import { CheckoutInfo } from '../../../application/ports/ICheckoutService';
import { Result } from '../../../../shared/result/Result';
import { CheckoutPrice } from '../../../domain/value-objects/CheckoutPrice';
import { CheckoutState } from '../../../domain/value-objects/CheckoutState';
import type { CheckoutInfoDTO } from '../../../application/dto/CheckoutInfoDTO';
import { IRACING_SELECTORS } from './dom/IRacingSelectors';
interface Page {
@@ -22,7 +22,7 @@ export class CheckoutPriceExtractor {
constructor(private readonly page: Page) {}
async extractCheckoutInfo(): Promise<Result<CheckoutInfo>> {
async extractCheckoutInfo(): Promise<Result<CheckoutInfoDTO>> {
try {
// Prefer the explicit pill element which contains the price
const pillLocator = this.page.locator('.label-pill, .label-inverse');

View File

@@ -1,10 +1,10 @@
import { Page } from 'playwright';
import { ILogger } from '../../../../application/ports/ILogger';
import { LoggerPort } from '@gridpilot/automation/application/ports/LoggerPort';
export class AuthenticationGuard {
constructor(
private readonly page: Page,
private readonly logger?: ILogger
private readonly logger?: LoggerPort
) {}
async checkForLoginUI(): Promise<boolean> {

View File

@@ -1,11 +1,11 @@
import type { Page } from 'playwright';
import type { ILogger } from '../../../../application/ports/ILogger';
import type { LoggerPort } from '@gridpilot/automation/application/ports/LoggerPort';
import type { IPlaywrightAuthFlow } from './PlaywrightAuthFlow';
import { IRACING_URLS, IRACING_SELECTORS, IRACING_TIMEOUTS } from '../dom/IRacingSelectors';
import { AuthenticationGuard } from './AuthenticationGuard';
export class IRacingPlaywrightAuthFlow implements IPlaywrightAuthFlow {
constructor(private readonly logger?: ILogger) {}
constructor(private readonly logger?: LoggerPort) {}
getLoginUrl(): string {
return IRACING_URLS.login;

View File

@@ -1,11 +1,11 @@
import * as fs from 'fs';
import type { BrowserContext, Page } from 'playwright';
import type { IAuthenticationService } from '../../../../application/ports/IAuthenticationService';
import type { ILogger } from '../../../../application/ports/ILogger';
import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState';
import { BrowserAuthenticationState } from '@gridpilot/automation/domain/value-objects/BrowserAuthenticationState';
import { Result } from '../../../../shared/result/Result';
import type { AuthenticationServicePort } from '../../../../application/ports/AuthenticationServicePort';
import type { LoggerPort } from '../../../../application/ports/LoggerPort';
import { AuthenticationState } from '../../../../domain/value-objects/AuthenticationState';
import { BrowserAuthenticationState } from '../../../../domain/value-objects/BrowserAuthenticationState';
import { Result } from '../../../../../shared/result/Result';
import { PlaywrightBrowserSession } from '../core/PlaywrightBrowserSession';
import { SessionCookieStore } from './SessionCookieStore';
import type { IPlaywrightAuthFlow } from './PlaywrightAuthFlow';
@@ -26,11 +26,11 @@ interface PlaywrightAuthSessionConfig {
* - Cookie persistence via SessionCookieStore
* - Exposing the IAuthenticationService port for application layer
*/
export class PlaywrightAuthSessionService implements IAuthenticationService {
export class PlaywrightAuthSessionService implements AuthenticationServicePort {
private readonly browserSession: PlaywrightBrowserSession;
private readonly cookieStore: SessionCookieStore;
private readonly authFlow: IPlaywrightAuthFlow;
private readonly logger?: ILogger;
private readonly logger?: LoggerPort;
private readonly navigationTimeoutMs: number;
private readonly loginWaitTimeoutMs: number;
@@ -41,7 +41,7 @@ export class PlaywrightAuthSessionService implements IAuthenticationService {
browserSession: PlaywrightBrowserSession,
cookieStore: SessionCookieStore,
authFlow: IPlaywrightAuthFlow,
logger?: ILogger,
logger?: LoggerPort,
config?: PlaywrightAuthSessionConfig,
) {
this.browserSession = browserSession;

View File

@@ -1,9 +1,9 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState';
import { CookieConfiguration } from '@gridpilot/automation/domain/value-objects/CookieConfiguration';
import { Result } from '../../../../shared/result/Result';
import type { ILogger } from '../../../../application/ports/ILogger';
import { AuthenticationState } from '../../../../domain/value-objects/AuthenticationState';
import { CookieConfiguration } from '../../../../domain/value-objects/CookieConfiguration';
import { Result } from '../../../../../shared/result/Result';
import type { LoggerPort } from '../../../../application/ports/LoggerPort';
interface Cookie {
name: string;
@@ -43,9 +43,9 @@ const EXPIRY_BUFFER_SECONDS = 300;
export class SessionCookieStore {
private readonly storagePath: string;
private logger?: ILogger;
constructor(userDataDir: string, logger?: ILogger) {
private logger?: LoggerPort;
constructor(userDataDir: string, logger?: LoggerPort) {
this.storagePath = path.join(userDataDir, 'session-state.json');
this.logger = logger;
}

View File

@@ -1,24 +1,22 @@
import type { Browser, Page, BrowserContext } from 'playwright';
import * as fs from 'fs';
import * as path from 'path';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState';
import { BrowserAuthenticationState } from '@gridpilot/automation/domain/value-objects/BrowserAuthenticationState';
import { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice';
import { CheckoutState } from '@gridpilot/automation/domain/value-objects/CheckoutState';
import { CheckoutConfirmation } from '@gridpilot/automation/domain/value-objects/CheckoutConfirmation';
import type { IBrowserAutomation } from '../../../../application/ports/IScreenAutomation';
import type {
NavigationResult,
FormFillResult,
ClickResult,
WaitResult,
ModalResult,
AutomationResult,
} from '../../../../application/ports/AutomationResults';
import type { IAuthenticationService } from '../../../../application/ports/IAuthenticationService';
import type { ILogger } from '../../../../application/ports/ILogger';
import { Result } from '../../../../shared/result/Result';
import { StepId } from '../../../../domain/value-objects/StepId';
import { AuthenticationState } from '../../../../domain/value-objects/AuthenticationState';
import { BrowserAuthenticationState } from '../../../../domain/value-objects/BrowserAuthenticationState';
import { CheckoutPrice } from '../../../../domain/value-objects/CheckoutPrice';
import { CheckoutState } from '../../../../domain/value-objects/CheckoutState';
import { CheckoutConfirmation } from '../../../../domain/value-objects/CheckoutConfirmation';
import type { IBrowserAutomation } from '../../../../application/ports/ScreenAutomationPort';
import type { NavigationResultDTO } from '../../../../application/dto/NavigationResultDTO';
import type { FormFillResultDTO } from '../../../../application/dto/FormFillResultDTO';
import type { ClickResultDTO } from '../../../../application/dto/ClickResultDTO';
import type { WaitResultDTO } from '../../../../application/dto/WaitResultDTO';
import type { ModalResultDTO } from '../../../../application/dto/ModalResultDTO';
import type { AutomationResultDTO } from '../../../../application/dto/AutomationResultDTO';
import type { AuthenticationServicePort } from '../../../../application/ports/AuthenticationServicePort';
import type { LoggerPort } from '../../../../application/ports/LoggerPort';
import { Result } from '../../../../../shared/result/Result';
import { IRACING_SELECTORS, IRACING_URLS, IRACING_TIMEOUTS, ALL_BLOCKED_SELECTORS, BLOCKED_KEYWORDS } from '../dom/IRacingSelectors';
import { SessionCookieStore } from '../auth/SessionCookieStore';
import { PlaywrightBrowserSession } from './PlaywrightBrowserSession';
@@ -421,7 +419,7 @@ export interface PlaywrightConfig {
userDataDir?: string;
}
export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthenticationService {
export class PlaywrightAutomationAdapter implements IBrowserAutomation, AuthenticationServicePort {
private browser: Browser | null = null;
private persistentContext: BrowserContext | null = null;
private context: BrowserContext | null = null;
@@ -430,7 +428,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
private browserSession: PlaywrightBrowserSession;
private connected = false;
private isConnecting = false;
private logger?: ILogger;
private logger?: LoggerPort;
private cookieStore: SessionCookieStore;
private authService: PlaywrightAuthSessionService;
private overlayInjected = false;
@@ -450,7 +448,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
private domInteractor!: IRacingDomInteractor;
private readonly stepOrchestrator: WizardStepOrchestrator;
constructor(config: PlaywrightConfig = {}, logger?: ILogger, browserModeLoader?: BrowserModeConfigLoader) {
constructor(config: PlaywrightConfig = {}, logger?: LoggerPort, browserModeLoader?: BrowserModeConfigLoader) {
this.config = {
headless: true,
timeout: 10000,
@@ -623,7 +621,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
this.connected = this.browserSession.isConnected();
}
async connect(forceHeaded: boolean = false): Promise<AutomationResult> {
async connect(forceHeaded: boolean = false): Promise<AutomationResultDTO> {
const result = await this.browserSession.connect(forceHeaded);
if (!result.success) {
return { success: false, error: result.error };
@@ -701,7 +699,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
return this.connected && this.page !== null;
}
async navigateToPage(url: string): Promise<NavigationResult> {
async navigateToPage(url: string): Promise<NavigationResultDTO> {
const result = await this.navigator.navigateToPage(url);
if (result.success) {
// Reset overlay state after successful navigation (page context changed)
@@ -710,7 +708,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
return result;
}
async fillFormField(fieldName: string, value: string): Promise<FormFillResult> {
async fillFormField(fieldName: string, value: string): Promise<FormFillResultDTO> {
return this.domInteractor.fillFormField(fieldName, value);
}
@@ -727,7 +725,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
return fieldMap[fieldName] || IRACING_SELECTORS.fields.textInput;
}
async clickElement(target: string): Promise<ClickResult> {
async clickElement(target: string): Promise<ClickResultDTO> {
return this.domInteractor.clickElement(target);
}
@@ -749,15 +747,15 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
return actionMap[action] || `button:has-text("${action}")`;
}
async waitForElement(target: string, maxWaitMs?: number): Promise<WaitResult> {
async waitForElement(target: string, maxWaitMs?: number): Promise<WaitResultDTO> {
return this.navigator.waitForElement(target, maxWaitMs);
}
async handleModal(stepId: StepId, action: string): Promise<ModalResult> {
async handleModal(stepId: StepId, action: string): Promise<ModalResultDTO> {
return this.domInteractor.handleModal(stepId, action);
}
async executeStep(stepId: StepId, config: Record<string, unknown>): Promise<AutomationResult> {
async executeStep(stepId: StepId, config: Record<string, unknown>): Promise<AutomationResultDTO> {
const stepNumber = stepId.value;
const skipFixtureNavigation =
(config as any).__skipFixtureNavigation === true;
@@ -1989,7 +1987,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
* First checks if user is already authenticated - if so, navigates directly to hosted sessions.
* Otherwise navigates to login page and waits for user to complete manual login.
*/
private async handleLogin(): Promise<AutomationResult> {
private async handleLogin(): Promise<AutomationResultDTO> {
try {
if (this.config.baseUrl && !this.config.baseUrl.includes('members.iracing.com')) {
this.log('info', 'Fixture baseUrl detected, treating session as authenticated for Step 1', {
@@ -2120,7 +2118,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
* Tries the primary selector first, then falls back to alternative selectors.
* This is needed because iRacing's form structure can vary slightly.
*/
private async fillFieldWithFallback(fieldName: string, value: string): Promise<FormFillResult> {
private async fillFieldWithFallback(fieldName: string, value: string): Promise<FormFillResultDTO> {
if (!this.page) {
return { success: false, fieldName, valueSet: value, error: 'Browser not connected' };
}
@@ -2224,7 +2222,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
}
}
async clickAction(action: string): Promise<ClickResult> {
async clickAction(action: string): Promise<ClickResultDTO> {
if (!this.page) {
return { success: false, target: action, error: 'Browser not connected' };
}
@@ -2253,7 +2251,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
return { success: true, target: selector };
}
async fillField(fieldName: string, value: string): Promise<FormFillResult> {
async fillField(fieldName: string, value: string): Promise<FormFillResultDTO> {
if (!this.page) {
return { success: false, fieldName, valueSet: value, error: 'Browser not connected' };
}

View File

@@ -4,7 +4,7 @@ import StealthPlugin from 'puppeteer-extra-plugin-stealth';
import * as fs from 'fs';
import * as path from 'path';
import type { ILogger } from '../../../../application/ports/ILogger';
import type { LoggerPort } from '@gridpilot/automation/application/ports/LoggerPort';
import { BrowserModeConfigLoader, BrowserMode } from '../../../config/BrowserModeConfig';
import { getAutomationMode } from '../../../config/AutomationConfig';
import type { PlaywrightConfig } from './PlaywrightAutomationAdapter';
@@ -27,7 +27,7 @@ export class PlaywrightBrowserSession {
constructor(
private readonly config: Required<PlaywrightConfig>,
private readonly logger?: ILogger,
private readonly logger?: LoggerPort,
browserModeLoader?: BrowserModeConfigLoader,
) {
const automationMode = getAutomationMode();

View File

@@ -1,15 +1,13 @@
import type { Page } from 'playwright';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import type {
AutomationResult,
ClickResult,
FormFillResult,
} from '../../../../application/ports/AutomationResults';
import type { IAuthenticationService } from '../../../../application/ports/IAuthenticationService';
import type { ILogger } from '../../../../application/ports/ILogger';
import { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice';
import { CheckoutState } from '@gridpilot/automation/domain/value-objects/CheckoutState';
import { CheckoutConfirmation } from '@gridpilot/automation/domain/value-objects/CheckoutConfirmation';
import { StepId } from '../../../../domain/value-objects/StepId';
import type { AutomationResultDTO } from '../../../../application/dto/AutomationResultDTO';
import type { ClickResultDTO } from '../../../../application/dto/ClickResultDTO';
import type { FormFillResultDTO } from '../../../../application/dto/FormFillResultDTO';
import type { AuthenticationServicePort } from '../../../../application/ports/AuthenticationServicePort';
import type { LoggerPort } from '../../../../application/ports/LoggerPort';
import { CheckoutPrice } from '../../../../domain/value-objects/CheckoutPrice';
import { CheckoutState } from '../../../../domain/value-objects/CheckoutState';
import { CheckoutConfirmation } from '../../../../domain/value-objects/CheckoutConfirmation';
import type { PlaywrightConfig } from './PlaywrightAutomationAdapter';
import { PlaywrightBrowserSession } from './PlaywrightBrowserSession';
import { IRacingDomNavigator } from '../dom/IRacingDomNavigator';
@@ -19,16 +17,16 @@ import { getFixtureForStep } from '../engine/FixtureServer';
import type {
PageStateValidation,
PageStateValidationResult,
} from '@gridpilot/automation/domain/services/PageStateValidator';
import type { Result } from '../../../../shared/result/Result';
} from '../../../../domain/services/PageStateValidator';
import type { Result } from '../../../../../shared/result/Result';
interface WizardStepOrchestratorDeps {
config: Required<PlaywrightConfig>;
browserSession: PlaywrightBrowserSession;
navigator: IRacingDomNavigator;
interactor: IRacingDomInteractor;
authService: IAuthenticationService;
logger?: ILogger;
authService: AuthenticationServicePort;
logger?: LoggerPort;
totalSteps: number;
getCheckoutConfirmationCallback: () =>
| ((
@@ -56,7 +54,7 @@ interface WizardStepOrchestratorDeps {
dismissDatetimePickers(): Promise<void>;
};
helpers: {
handleLogin(): Promise<AutomationResult>;
handleLogin(): Promise<AutomationResultDTO>;
validatePageState(
validation: PageStateValidation,
): Promise<Result<PageStateValidationResult, Error>>;
@@ -69,8 +67,8 @@ export class WizardStepOrchestrator {
private readonly browserSession: PlaywrightBrowserSession;
private readonly navigator: IRacingDomNavigator;
private readonly interactor: IRacingDomInteractor;
private readonly authService: IAuthenticationService;
private readonly logger?: ILogger;
private readonly authService: AuthenticationServicePort;
private readonly logger?: LoggerPort;
private readonly totalSteps: number;
private readonly getCheckoutConfirmationCallbackInternal: WizardStepOrchestratorDeps['getCheckoutConfirmationCallback'];
private readonly overlay: WizardStepOrchestratorDeps['overlay'];
@@ -139,7 +137,7 @@ export class WizardStepOrchestrator {
await this.guards.dismissModals();
}
private async handleLogin(): Promise<AutomationResult> {
private async handleLogin(): Promise<AutomationResultDTO> {
return this.helpers.handleLogin();
}
@@ -147,14 +145,14 @@ export class WizardStepOrchestrator {
await this.navigator.waitForStep(stepNumber);
}
private async clickAction(action: string): Promise<ClickResult> {
private async clickAction(action: string): Promise<ClickResultDTO> {
return this.interactor.clickAction(action);
}
private async fillFieldWithFallback(
fieldName: string,
value: string,
): Promise<FormFillResult> {
): Promise<FormFillResultDTO> {
return this.interactor.fillFieldWithFallback(fieldName, value);
}
@@ -200,7 +198,7 @@ export class WizardStepOrchestrator {
private async fillField(
fieldName: string,
value: string,
): Promise<FormFillResult> {
): Promise<FormFillResultDTO> {
return this.interactor.fillField(fieldName, value);
}
@@ -266,7 +264,7 @@ export class WizardStepOrchestrator {
async executeStep(
stepId: StepId,
config: Record<string, unknown>,
): Promise<AutomationResult> {
): Promise<AutomationResultDTO> {
if (!this.page) {
return { success: false, error: 'Browser not connected' };
}

View File

@@ -1,11 +1,9 @@
import type { Page } from 'playwright';
import type { ILogger } from '../../../../application/ports/ILogger';
import type {
FormFillResult,
ClickResult,
ModalResult,
} from '../../../../application/ports/AutomationResults';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import type { LoggerPort } from '../../../../application/ports/LoggerPort';
import type { FormFillResultDTO } from '../../../../application/dto/FormFillResultDTO';
import type { ClickResultDTO } from '../../../../application/dto/ClickResultDTO';
import type { ModalResultDTO } from '../../../../application/dto/ModalResultDTO';
import { StepId } from '../../../../domain/value-objects/StepId';
import type { PlaywrightConfig } from '../core/PlaywrightAutomationAdapter';
import { PlaywrightBrowserSession } from '../core/PlaywrightBrowserSession';
import { IRACING_SELECTORS, IRACING_TIMEOUTS } from './IRacingSelectors';
@@ -17,7 +15,7 @@ export class IRacingDomInteractor {
private readonly config: Required<PlaywrightConfig>,
private readonly browserSession: PlaywrightBrowserSession,
private readonly safeClickService: SafeClickService,
private readonly logger?: ILogger,
private readonly logger?: LoggerPort,
) {}
private log(level: 'debug' | 'info' | 'warn' | 'error', message: string, context?: Record<string, unknown>): void {
@@ -42,7 +40,7 @@ export class IRacingDomInteractor {
// ===== Public port-facing operations =====
async fillFormField(fieldName: string, value: string): Promise<FormFillResult> {
async fillFormField(fieldName: string, value: string): Promise<FormFillResultDTO> {
const page = this.browserSession.getPage();
if (!page) {
return { success: false, fieldName, valueSet: value, error: 'Browser not connected' };
@@ -104,7 +102,7 @@ export class IRacingDomInteractor {
}
}
async clickElement(target: string): Promise<ClickResult> {
async clickElement(target: string): Promise<ClickResultDTO> {
const page = this.browserSession.getPage();
if (!page) {
return { success: false, target, error: 'Browser not connected' };
@@ -124,7 +122,7 @@ export class IRacingDomInteractor {
}
}
async handleModal(stepId: StepId, action: string): Promise<ModalResult> {
async handleModal(stepId: StepId, action: string): Promise<ModalResultDTO> {
const page = this.browserSession.getPage();
if (!page) {
return { success: false, stepId: stepId.value, action, error: 'Browser not connected' };
@@ -156,7 +154,7 @@ export class IRacingDomInteractor {
// ===== Public interaction helpers used by adapter steps =====
async fillField(fieldName: string, value: string): Promise<FormFillResult> {
async fillField(fieldName: string, value: string): Promise<FormFillResultDTO> {
const page = this.browserSession.getPage();
if (!page) {
return { success: false, fieldName, valueSet: value, error: 'Browser not connected' };
@@ -208,7 +206,7 @@ export class IRacingDomInteractor {
}
}
async fillFieldWithFallback(fieldName: string, value: string): Promise<FormFillResult> {
async fillFieldWithFallback(fieldName: string, value: string): Promise<FormFillResultDTO> {
const page = this.browserSession.getPage();
if (!page) {
return { success: false, fieldName, valueSet: value, error: 'Browser not connected' };
@@ -249,7 +247,7 @@ export class IRacingDomInteractor {
}
}
async clickAction(action: string): Promise<ClickResult> {
async clickAction(action: string): Promise<ClickResultDTO> {
const page = this.browserSession.getPage();
if (!page) {
return { success: false, target: action, error: 'Browser not connected' };

View File

@@ -1,6 +1,7 @@
import type { Page } from 'playwright';
import type { ILogger } from '../../../../application/ports/ILogger';
import type { NavigationResult, WaitResult } from '../../../../application/ports/AutomationResults';
import type { LoggerPort } from '../../../../application/ports/LoggerPort';
import type { NavigationResultDTO } from '../../../../application/dto/NavigationResultDTO';
import type { WaitResultDTO } from '../../../../application/dto/WaitResultDTO';
import type { PlaywrightConfig } from '../core/PlaywrightAutomationAdapter';
import { PlaywrightBrowserSession } from '../core/PlaywrightBrowserSession';
import { IRACING_SELECTORS, IRACING_TIMEOUTS, IRACING_URLS } from './IRacingSelectors';
@@ -23,7 +24,7 @@ export class IRacingDomNavigator {
constructor(
private readonly config: Required<PlaywrightConfig>,
private readonly browserSession: PlaywrightBrowserSession,
private readonly logger?: ILogger,
private readonly logger?: LoggerPort,
private readonly onWizardDismissed?: () => Promise<void>,
) {}
@@ -43,7 +44,7 @@ export class IRacingDomNavigator {
return this.browserSession.getPage();
}
async navigateToPage(url: string): Promise<NavigationResult> {
async navigateToPage(url: string): Promise<NavigationResultDTO> {
const page = this.getPage();
if (!page) {
return { success: false, url, loadTime: 0, error: 'Browser not connected' };
@@ -78,7 +79,7 @@ export class IRacingDomNavigator {
}
}
async waitForElement(target: string, maxWaitMs?: number): Promise<WaitResult> {
async waitForElement(target: string, maxWaitMs?: number): Promise<WaitResultDTO> {
const page = this.getPage();
if (!page) {
return { success: false, target, waitedMs: 0, found: false, error: 'Browser not connected' };

View File

@@ -1,5 +1,5 @@
import type { Page } from 'playwright';
import type { ILogger } from '../../../../application/ports/ILogger';
import type { LoggerPort } from '@gridpilot/automation/application/ports/LoggerPort';
import { IRACING_SELECTORS, BLOCKED_KEYWORDS } from './IRacingSelectors';
import type { PlaywrightConfig } from '../core/PlaywrightAutomationAdapter';
import { PlaywrightBrowserSession } from '../core/PlaywrightBrowserSession';
@@ -8,7 +8,7 @@ export class SafeClickService {
constructor(
private readonly config: Required<PlaywrightConfig>,
private readonly browserSession: PlaywrightBrowserSession,
private readonly logger?: ILogger,
private readonly logger?: LoggerPort,
) {}
private log(level: 'debug' | 'info' | 'warn' | 'error', message: string, context?: Record<string, unknown>): void {

View File

@@ -1,9 +1,14 @@
import { IAutomationEngine, ValidationResult } from '../../../../application/ports/IAutomationEngine';
import { HostedSessionConfig } from '@gridpilot/automation/domain/entities/HostedSessionConfig';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import type { IBrowserAutomation } from '../../../../application/ports/IScreenAutomation';
import { ISessionRepository } from '../../../../application/ports/ISessionRepository';
import { StepTransitionValidator } from '@gridpilot/automation/domain/services/StepTransitionValidator';
import type { AutomationEnginePort } from '../../../../application/ports/AutomationEnginePort';
import { HostedSessionConfig } from '../../../../domain/entities/HostedSessionConfig';
import { StepId } from '../../../../domain/value-objects/StepId';
import type { IBrowserAutomation } from '../../../../application/ports/ScreenAutomationPort';
import type { SessionRepositoryPort } from '../../../../application/ports/SessionRepositoryPort';
import { StepTransitionValidator } from '../../../../domain/services/StepTransitionValidator';
type ValidationResult = {
isValid: boolean;
error?: string;
};
/**
* Real Automation Engine Adapter.
@@ -22,13 +27,13 @@ import { StepTransitionValidator } from '@gridpilot/automation/domain/services/S
* browser automation when available. See docs/ARCHITECTURE.md
* for the updated automation strategy.
*/
export class AutomationEngineAdapter implements IAutomationEngine {
export class AutomationEngineAdapter implements AutomationEnginePort {
private isRunning = false;
private automationPromise: Promise<void> | null = null;
constructor(
private readonly browserAutomation: IBrowserAutomation,
private readonly sessionRepository: ISessionRepository
private readonly sessionRepository: SessionRepositoryPort
) {}
async validateConfiguration(config: HostedSessionConfig): Promise<ValidationResult> {

View File

@@ -1,17 +1,22 @@
import { IAutomationEngine, ValidationResult } from '../../../../application/ports/IAutomationEngine';
import { HostedSessionConfig } from '@gridpilot/automation/domain/entities/HostedSessionConfig';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import type { IBrowserAutomation } from '../../../../application/ports/IScreenAutomation';
import { ISessionRepository } from '../../../../application/ports/ISessionRepository';
import { StepTransitionValidator } from '@gridpilot/automation/domain/services/StepTransitionValidator';
import type { AutomationEnginePort } from '../../../../application/ports/AutomationEnginePort';
import { HostedSessionConfig } from '../../../../domain/entities/HostedSessionConfig';
import { StepId } from '../../../../domain/value-objects/StepId';
import type { IBrowserAutomation } from '../../../../application/ports/ScreenAutomationPort';
import type { SessionRepositoryPort } from '../../../../application/ports/SessionRepositoryPort';
import { StepTransitionValidator } from '../../../../domain/services/StepTransitionValidator';
export class MockAutomationEngineAdapter implements IAutomationEngine {
type ValidationResult = {
isValid: boolean;
error?: string;
};
export class MockAutomationEngineAdapter implements AutomationEnginePort {
private isRunning = false;
private automationPromise: Promise<void> | null = null;
constructor(
private readonly browserAutomation: IBrowserAutomation,
private readonly sessionRepository: ISessionRepository
private readonly sessionRepository: SessionRepositoryPort
) {}
async validateConfiguration(config: HostedSessionConfig): Promise<ValidationResult> {

View File

@@ -1,13 +1,11 @@
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import type { IBrowserAutomation } from '../../../../application/ports/IScreenAutomation';
import {
NavigationResult,
FormFillResult,
ClickResult,
WaitResult,
ModalResult,
AutomationResult,
} from '../../../../application/ports/AutomationResults';
import { StepId } from '../../../../domain/value-objects/StepId';
import type { IBrowserAutomation } from '../../../../application/ports/ScreenAutomationPort';
import type { NavigationResultDTO } from '../../../../application/dto/NavigationResultDTO';
import type { FormFillResultDTO } from '../../../../application/dto/FormFillResultDTO';
import type { ClickResultDTO } from '../../../../application/dto/ClickResultDTO';
import type { WaitResultDTO } from '../../../../application/dto/WaitResultDTO';
import type { ModalResultDTO } from '../../../../application/dto/ModalResultDTO';
import type { AutomationResultDTO } from '../../../../application/dto/AutomationResultDTO';
interface MockConfig {
simulateFailures?: boolean;
@@ -37,7 +35,7 @@ export class MockBrowserAutomationAdapter implements IBrowserAutomation {
};
}
async connect(): Promise<AutomationResult> {
async connect(): Promise<AutomationResultDTO> {
this.connected = true;
return { success: true };
}
@@ -50,7 +48,7 @@ export class MockBrowserAutomationAdapter implements IBrowserAutomation {
return this.connected;
}
async navigateToPage(url: string): Promise<NavigationResult> {
async navigateToPage(url: string): Promise<NavigationResultDTO> {
const delay = this.randomDelay(200, 800);
await this.sleep(delay);
return {
@@ -60,7 +58,7 @@ export class MockBrowserAutomationAdapter implements IBrowserAutomation {
};
}
async fillFormField(fieldName: string, value: string): Promise<FormFillResult> {
async fillFormField(fieldName: string, value: string): Promise<FormFillResultDTO> {
const delay = this.randomDelay(100, 500);
await this.sleep(delay);
return {
@@ -70,7 +68,7 @@ export class MockBrowserAutomationAdapter implements IBrowserAutomation {
};
}
async clickElement(selector: string): Promise<ClickResult> {
async clickElement(selector: string): Promise<ClickResultDTO> {
const delay = this.randomDelay(50, 300);
await this.sleep(delay);
return {
@@ -79,7 +77,7 @@ export class MockBrowserAutomationAdapter implements IBrowserAutomation {
};
}
async waitForElement(selector: string, maxWaitMs: number = 5000): Promise<WaitResult> {
async waitForElement(selector: string, maxWaitMs: number = 5000): Promise<WaitResultDTO> {
const delay = this.randomDelay(100, 1000);
await this.sleep(delay);
@@ -92,7 +90,7 @@ export class MockBrowserAutomationAdapter implements IBrowserAutomation {
};
}
async handleModal(stepId: StepId, action: string): Promise<ModalResult> {
async handleModal(stepId: StepId, action: string): Promise<ModalResultDTO> {
if (!stepId.isModalStep()) {
throw new Error(`Step ${stepId.value} is not a modal step`);
}
@@ -106,7 +104,7 @@ export class MockBrowserAutomationAdapter implements IBrowserAutomation {
};
}
async executeStep(stepId: StepId, config: Record<string, unknown>): Promise<AutomationResult> {
async executeStep(stepId: StepId, config: Record<string, unknown>): Promise<AutomationResultDTO> {
if (this.shouldSimulateFailure()) {
throw new Error(`Simulated failure at step ${stepId.value}`);
}

View File

@@ -5,11 +5,12 @@
import type { BrowserWindow } from 'electron';
import { ipcMain } from 'electron';
import { Result } from '../../../shared/result/Result';
import type { ICheckoutConfirmationPort, CheckoutConfirmationRequest } from '../../../application/ports/ICheckoutConfirmationPort';
import { CheckoutConfirmation } from '@gridpilot/automation/domain/value-objects/CheckoutConfirmation';
import { Result } from '../../../../shared/result/Result';
import type { CheckoutConfirmationPort } from '../../../application/ports/CheckoutConfirmationPort';
import type { CheckoutConfirmationRequestDTO } from '../../../application/dto/CheckoutConfirmationRequestDTO';
import { CheckoutConfirmation } from '../../../domain/value-objects/CheckoutConfirmation';
export class ElectronCheckoutConfirmationAdapter implements ICheckoutConfirmationPort {
export class ElectronCheckoutConfirmationAdapter implements CheckoutConfirmationPort {
private mainWindow: BrowserWindow;
private pendingConfirmation: {
resolve: (confirmation: CheckoutConfirmation) => void;
@@ -40,7 +41,7 @@ export class ElectronCheckoutConfirmationAdapter implements ICheckoutConfirmatio
}
async requestCheckoutConfirmation(
request: CheckoutConfirmationRequest
request: CheckoutConfirmationRequestDTO
): Promise<Result<CheckoutConfirmation>> {
try {
// Only allow one pending confirmation at a time

View File

@@ -1,6 +1,7 @@
import type { ILogger, LogContext } from '../../../application/ports/ILogger';
import type { LoggerPort } from '../../../application/ports/LoggerPort';
import type { LogContext } from '../../../application/ports/LoggerContext';
export class NoOpLogAdapter implements ILogger {
export class NoOpLogAdapter implements LoggerPort {
debug(_message: string, _context?: LogContext): void {}
info(_message: string, _context?: LogContext): void {}
@@ -11,7 +12,7 @@ export class NoOpLogAdapter implements ILogger {
fatal(_message: string, _error?: Error, _context?: LogContext): void {}
child(_context: LogContext): ILogger {
child(_context: LogContext): LoggerPort {
return this;
}

View File

@@ -1,4 +1,6 @@
import type { ILogger, LogContext, LogLevel } from '../../../application/ports/ILogger';
import type { LoggerPort } from '@gridpilot/automation/application/ports/LoggerPort';
import type { LogContext } from '@gridpilot/automation/application/ports/LoggerContext';
import type { LogLevel } from '@gridpilot/automation/application/ports/LoggerLogLevel';
import { loadLoggingConfig, type LoggingEnvironmentConfig } from '../../config/LoggingConfig';
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
@@ -18,7 +20,7 @@ const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
*
* This provides structured JSON logging to stdout with the same interface.
*/
export class PinoLogAdapter implements ILogger {
export class PinoLogAdapter implements LoggerPort {
private readonly config: LoggingEnvironmentConfig;
private readonly baseContext: LogContext;
private readonly levelPriority: number;
@@ -106,7 +108,7 @@ export class PinoLogAdapter implements ILogger {
this.log('fatal', message, context, error);
}
child(context: LogContext): ILogger {
child(context: LogContext): LoggerPort {
return new PinoLogAdapter(this.config, { ...this.baseContext, ...context });
}