wip
This commit is contained in:
@@ -24,14 +24,16 @@ export class StartAutomationSessionUseCase
|
||||
|
||||
await this.sessionRepository.save(session);
|
||||
|
||||
return {
|
||||
const dto: SessionDTO = {
|
||||
sessionId: session.id,
|
||||
state: session.state.value,
|
||||
currentStep: session.currentStep.value,
|
||||
config: session.config,
|
||||
startedAt: session.startedAt,
|
||||
completedAt: session.completedAt,
|
||||
errorMessage: session.errorMessage,
|
||||
...(session.startedAt ? { startedAt: session.startedAt } : {}),
|
||||
...(session.completedAt ? { completedAt: session.completedAt } : {}),
|
||||
...(session.errorMessage ? { errorMessage: session.errorMessage } : {}),
|
||||
};
|
||||
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,18 @@ export class CheckoutConfirmation {
|
||||
return new CheckoutConfirmation(value);
|
||||
}
|
||||
|
||||
static confirmed(): CheckoutConfirmation {
|
||||
return CheckoutConfirmation.create('confirmed');
|
||||
}
|
||||
|
||||
static cancelled(_reason?: string): CheckoutConfirmation {
|
||||
return CheckoutConfirmation.create('cancelled');
|
||||
}
|
||||
|
||||
static timeout(): CheckoutConfirmation {
|
||||
return CheckoutConfirmation.create('timeout');
|
||||
}
|
||||
|
||||
get value(): CheckoutConfirmationDecision {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
@@ -54,10 +54,6 @@ export class SessionState implements IValueObject<SessionStateProps> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
equals(other: SessionState): boolean {
|
||||
return this._value === other._value;
|
||||
}
|
||||
|
||||
isPending(): boolean {
|
||||
return this._value === 'PENDING';
|
||||
}
|
||||
|
||||
@@ -25,10 +25,6 @@ export class StepId implements IValueObject<StepIdProps> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
equals(other: StepId): boolean {
|
||||
return this._value === other._value;
|
||||
}
|
||||
|
||||
isModalStep(): boolean {
|
||||
return this._value === 6 || this._value === 9 || this._value === 12;
|
||||
}
|
||||
|
||||
@@ -27,10 +27,10 @@ interface PlaywrightAuthSessionConfig {
|
||||
* - Exposing the IAuthenticationService port for application layer
|
||||
*/
|
||||
export class PlaywrightAuthSessionService implements AuthenticationServicePort {
|
||||
private readonly browserSession: PlaywrightBrowserSession;
|
||||
private readonly cookieStore: SessionCookieStore;
|
||||
private readonly authFlow: IPlaywrightAuthFlow;
|
||||
private readonly logger?: LoggerPort;
|
||||
private readonly browserSession: PlaywrightBrowserSession;
|
||||
private readonly cookieStore: SessionCookieStore;
|
||||
private readonly authFlow: IPlaywrightAuthFlow;
|
||||
private readonly logger: LoggerPort | undefined;
|
||||
|
||||
private readonly navigationTimeoutMs: number;
|
||||
private readonly loginWaitTimeoutMs: number;
|
||||
|
||||
@@ -43,9 +43,9 @@ const EXPIRY_BUFFER_SECONDS = 300;
|
||||
|
||||
export class SessionCookieStore {
|
||||
private readonly storagePath: string;
|
||||
private logger?: LoggerPort;
|
||||
private readonly logger: LoggerPort | undefined;
|
||||
|
||||
constructor(userDataDir: string, logger?: LoggerPort) {
|
||||
constructor(userDataDir: string, logger: LoggerPort | undefined) {
|
||||
this.storagePath = path.join(userDataDir, 'session-state.json');
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@@ -428,7 +428,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, Authenti
|
||||
private browserSession: PlaywrightBrowserSession;
|
||||
private connected = false;
|
||||
private isConnecting = false;
|
||||
private logger?: LoggerPort;
|
||||
private logger: LoggerPort | undefined;
|
||||
private cookieStore: SessionCookieStore;
|
||||
private authService: PlaywrightAuthSessionService;
|
||||
private overlayInjected = false;
|
||||
@@ -438,7 +438,8 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, Authenti
|
||||
private static readonly PAUSE_CHECK_INTERVAL = 300;
|
||||
|
||||
/** Checkout confirmation callback - called before clicking checkout button */
|
||||
private checkoutConfirmationCallback?: (price: CheckoutPrice, state: CheckoutState) => Promise<CheckoutConfirmation>;
|
||||
private checkoutConfirmationCallback: (price: CheckoutPrice, state: CheckoutState) => Promise<CheckoutConfirmation> =
|
||||
async () => CheckoutConfirmation.cancelled('No checkout confirmation callback configured');
|
||||
|
||||
/** Page state validator instance */
|
||||
private pageStateValidator: PageStateValidator;
|
||||
@@ -448,7 +449,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, Authenti
|
||||
private domInteractor!: IRacingDomInteractor;
|
||||
private readonly stepOrchestrator: WizardStepOrchestrator;
|
||||
|
||||
constructor(config: PlaywrightConfig = {}, logger?: LoggerPort, browserModeLoader?: BrowserModeConfigLoader) {
|
||||
constructor(config: PlaywrightConfig = {}, logger: LoggerPort | undefined, browserModeLoader?: BrowserModeConfigLoader) {
|
||||
this.config = {
|
||||
headless: true,
|
||||
timeout: 10000,
|
||||
@@ -627,7 +628,8 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, Authenti
|
||||
async connect(forceHeaded: boolean = false): Promise<AutomationResultDTO> {
|
||||
const result = await this.browserSession.connect(forceHeaded);
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error };
|
||||
const errorMessage = result.error ?? 'Unknown automation connection error';
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
|
||||
this.syncSessionStateFromBrowser();
|
||||
@@ -1333,12 +1335,10 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, Authenti
|
||||
|
||||
// Get ONLY the direct text of this element, excluding child element text
|
||||
// This prevents false positives when a checkout button exists elsewhere on the page
|
||||
const directText = await element.evaluate((el) => {
|
||||
// Get only direct text nodes, not text from child elements
|
||||
const directText = await element.evaluate((el: Node) => {
|
||||
let text = '';
|
||||
const childNodes = Array.from(el.childNodes);
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const node = childNodes[i];
|
||||
const childNodes = Array.from((el as HTMLElement).childNodes);
|
||||
for (const node of childNodes) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
text += node.textContent || '';
|
||||
}
|
||||
@@ -2296,8 +2296,8 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, Authenti
|
||||
const el = document.querySelector(sel) as HTMLInputElement | HTMLTextAreaElement | null;
|
||||
if (!el) return;
|
||||
el.value = val;
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
(el as any).dispatchEvent(new Event('input', { bubbles: true }));
|
||||
(el as any).dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}, { sel: selector, val: value });
|
||||
return { success: true, fieldName, valueSet: value };
|
||||
} catch (evalErr) {
|
||||
@@ -2495,11 +2495,11 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, Authenti
|
||||
// If element is a checkbox/input, set checked; otherwise try to toggle aria-checked or click
|
||||
if ('checked' in el) {
|
||||
(el as HTMLInputElement).checked = Boolean(should);
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
(el as any).dispatchEvent(new Event('change', { bubbles: true }));
|
||||
} else {
|
||||
// Fallback: set aria-checked attribute and dispatch click
|
||||
(el as HTMLElement).setAttribute('aria-checked', String(Boolean(should)));
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
(el as any).dispatchEvent(new Event('change', { bubbles: true }));
|
||||
try { (el as HTMLElement).click(); } catch { /* ignore */ }
|
||||
}
|
||||
} catch {
|
||||
@@ -3013,7 +3013,12 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, Authenti
|
||||
setCheckoutConfirmationCallback(
|
||||
callback?: (price: CheckoutPrice, state: CheckoutState) => Promise<CheckoutConfirmation>
|
||||
): void {
|
||||
this.checkoutConfirmationCallback = callback;
|
||||
if (callback) {
|
||||
this.checkoutConfirmationCallback = callback;
|
||||
} else {
|
||||
this.checkoutConfirmationCallback = async () =>
|
||||
CheckoutConfirmation.cancelled('No checkout confirmation callback configured');
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Overlay Methods =====
|
||||
@@ -3085,7 +3090,13 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, Authenti
|
||||
const progress = Math.round((step / this.totalSteps) * 100);
|
||||
const personality = OVERLAY_PERSONALITY_MESSAGES[Math.floor(Math.random() * OVERLAY_PERSONALITY_MESSAGES.length)];
|
||||
|
||||
await this.page.evaluate(({ actionMsg, progressPct, stepNum, totalSteps, personalityMsg }) => {
|
||||
await this.page.evaluate(({ actionMsg, progressPct, stepNum, totalSteps, personalityMsg }: {
|
||||
actionMsg: string;
|
||||
progressPct: number;
|
||||
stepNum: number;
|
||||
totalSteps: number;
|
||||
personalityMsg: string | undefined;
|
||||
}) => {
|
||||
const actionEl = document.getElementById('gridpilot-action');
|
||||
const progressEl = document.getElementById('gridpilot-progress');
|
||||
const stepTextEl = document.getElementById('gridpilot-step-text');
|
||||
@@ -3094,9 +3105,9 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, Authenti
|
||||
|
||||
if (actionEl) actionEl.textContent = actionMsg;
|
||||
if (progressEl) progressEl.style.width = `${progressPct}%`;
|
||||
if (stepTextEl) stepTextEl.textContent = actionMsg;
|
||||
if (stepTextEl) stepTextEl.textContent = actionMsg ?? '';
|
||||
if (stepCountEl) stepCountEl.textContent = `Step ${stepNum} of ${totalSteps}`;
|
||||
if (personalityEl) personalityEl.textContent = personalityMsg;
|
||||
if (personalityEl) personalityEl.textContent = personalityMsg ?? '';
|
||||
}, {
|
||||
actionMsg: actionMessage,
|
||||
progressPct: progress,
|
||||
|
||||
@@ -26,7 +26,7 @@ interface WizardStepOrchestratorDeps {
|
||||
navigator: IRacingDomNavigator;
|
||||
interactor: IRacingDomInteractor;
|
||||
authService: AuthenticationServicePort;
|
||||
logger?: LoggerPort;
|
||||
logger?: LoggerPort | undefined;
|
||||
totalSteps: number;
|
||||
getCheckoutConfirmationCallback: () =>
|
||||
| ((
|
||||
@@ -68,7 +68,7 @@ export class WizardStepOrchestrator {
|
||||
private readonly navigator: IRacingDomNavigator;
|
||||
private readonly interactor: IRacingDomInteractor;
|
||||
private readonly authService: AuthenticationServicePort;
|
||||
private readonly logger?: LoggerPort;
|
||||
private readonly logger: LoggerPort | undefined;
|
||||
private readonly totalSteps: number;
|
||||
private readonly getCheckoutConfirmationCallbackInternal: WizardStepOrchestratorDeps['getCheckoutConfirmationCallback'];
|
||||
private readonly overlay: WizardStepOrchestratorDeps['overlay'];
|
||||
|
||||
@@ -63,7 +63,7 @@ export class IRacingDomInteractor {
|
||||
return { success: false, fieldName, valueSet: value, error: `Unknown form field: ${fieldName}` };
|
||||
}
|
||||
|
||||
const selector = fieldMap[fieldName];
|
||||
const selector = fieldMap[fieldName as keyof typeof fieldMap] ?? IRACING_SELECTORS.fields.textInput;
|
||||
const timeout = this.isRealMode() ? IRACING_TIMEOUTS.elementWait : this.config.timeout;
|
||||
|
||||
this.log('debug', 'Filling form field', { fieldName, selector, mode: this.config.mode });
|
||||
@@ -90,8 +90,8 @@ export class IRacingDomInteractor {
|
||||
const el = document.querySelector(sel) as HTMLInputElement | HTMLTextAreaElement | null;
|
||||
if (!el) return;
|
||||
el.value = val;
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
(el as any).dispatchEvent(new Event('input', { bubbles: true }));
|
||||
(el as any).dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}, { sel: selector, val: value });
|
||||
return { success: true, fieldName, valueSet: value };
|
||||
} catch (evalErr) {
|
||||
@@ -514,10 +514,10 @@ export class IRacingDomInteractor {
|
||||
try {
|
||||
if ('checked' in el) {
|
||||
(el as HTMLInputElement).checked = Boolean(should);
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
(el as any).dispatchEvent(new Event('change', { bubbles: true }));
|
||||
} else {
|
||||
(el as HTMLElement).setAttribute('aria-checked', String(Boolean(should)));
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
(el as any).dispatchEvent(new Event('change', { bubbles: true }));
|
||||
try {
|
||||
(el as HTMLElement).click();
|
||||
} catch {
|
||||
@@ -621,8 +621,10 @@ export class IRacingDomInteractor {
|
||||
try {
|
||||
el.value = String(val);
|
||||
el.setAttribute('data-value', String(val));
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
const inputEvent = new Event('input', { bubbles: true });
|
||||
const changeEvent = new Event('change', { bubbles: true });
|
||||
(el as any).dispatchEvent(inputEvent);
|
||||
(el as any).dispatchEvent(changeEvent);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
@@ -67,10 +67,13 @@ export class IRacingDomNavigator {
|
||||
if (!this.isRealMode()) {
|
||||
const stepMatch = url.match(/step-(\d+)-/);
|
||||
if (stepMatch) {
|
||||
const stepNumber = parseInt(stepMatch[1], 10);
|
||||
await page.evaluate((step) => {
|
||||
document.body.setAttribute('data-step', String(step));
|
||||
}, stepNumber);
|
||||
const [, stepStr] = stepMatch;
|
||||
if (stepStr) {
|
||||
const stepNumber = parseInt(stepStr, 10);
|
||||
await page.evaluate((step) => {
|
||||
document.body.setAttribute('data-step', String(step));
|
||||
}, stepNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -136,10 +136,9 @@ export class SafeClickService {
|
||||
.evaluate((el) => {
|
||||
let text = '';
|
||||
const childNodes = Array.from(el.childNodes);
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const node = childNodes[i];
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
text += node.textContent || '';
|
||||
for (const child of childNodes) {
|
||||
if (child.nodeType === Node.TEXT_NODE) {
|
||||
text += child.textContent || '';
|
||||
}
|
||||
}
|
||||
return text.trim();
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { AutomationEnginePort } from '../../../../application/ports/Automat
|
||||
import type { HostedSessionConfig } from '../../../../domain/types/HostedSessionConfig';
|
||||
import { StepId } from '../../../../domain/value-objects/StepId';
|
||||
import type { IBrowserAutomation } from '../../../../application/ports/ScreenAutomationPort';
|
||||
import type { HostedSessionConfig } from '../../../../domain/types/HostedSessionConfig';
|
||||
import type { SessionRepositoryPort } from '../../../../application/ports/SessionRepositoryPort';
|
||||
import { StepTransitionValidator } from '../../../../domain/services/StepTransitionValidator';
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ export class FixtureServer implements IFixtureServer {
|
||||
|
||||
let fileName: string;
|
||||
if (urlPath === '/') {
|
||||
fileName = STEP_TO_FIXTURE[1];
|
||||
fileName = STEP_TO_FIXTURE[1] ?? '01-hosted-racing.html';
|
||||
} else {
|
||||
fileName = urlPath.replace(/^\//, '');
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { AutomationEnginePort } from '../../../../application/ports/Automat
|
||||
import type { HostedSessionConfig } from '../../../../domain/types/HostedSessionConfig';
|
||||
import { StepId } from '../../../../domain/value-objects/StepId';
|
||||
import type { IBrowserAutomation } from '../../../../application/ports/ScreenAutomationPort';
|
||||
import type { HostedSessionConfig } from '../../../../domain/types/HostedSessionConfig';
|
||||
import type { SessionRepositoryPort } from '../../../../application/ports/SessionRepositoryPort';
|
||||
import { StepTransitionValidator } from '../../../../domain/services/StepTransitionValidator';
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ 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 { IAutomationLifecycleEmitter, LifecycleCallback } from '../../../IAutomationLifecycleEmitter';
|
||||
import type { IAutomationLifecycleEmitter, LifecycleCallback } from '../../IAutomationLifecycleEmitter';
|
||||
|
||||
interface MockConfig {
|
||||
simulateFailures?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user