import type { IDomainValidationService } from '@core/shared/domain'; import { Result } from '@core/shared/application/Result'; /** * Configuration for page state validation. * Defines expected and forbidden elements on the current page. */ export interface PageStateValidation { /** Expected wizard step name (e.g., 'cars', 'track') */ expectedStep: string; /** Selectors that MUST be present on the page */ requiredSelectors: string[]; /** Selectors that MUST NOT be present on the page */ forbiddenSelectors?: string[]; } /** * Result of page state validation. */ export interface PageStateValidationResult { isValid: boolean; message: string; expectedStep: string; missingSelectors?: string[]; unexpectedSelectors?: string[]; } export interface PageStateValidationInput { actualState: (_selector: string) => boolean; validation: PageStateValidation; realMode?: boolean; } /** * Domain service for validating page state during wizard navigation. * * Purpose: Prevent navigation bugs by ensuring each step executes on the correct page. * * Clean Architecture: This is pure domain logic with no infrastructure dependencies. * It validates state based on selector presence/absence without knowing HOW to check them. */ export class PageStateValidator implements IDomainValidationService { validate(input: PageStateValidationInput): Result { const { actualState, validation, realMode } = input; if (typeof realMode === 'boolean') { return this.validateStateEnhanced(actualState, validation, realMode); } return this.validateState(actualState, validation); } /** * Validate that the page state matches expected conditions. * * @param actualState Function that checks if selectors exist on the page * @param validation Expected page state configuration * @returns Result with validation outcome */ validateState( actualState: (_selector: string) => boolean, validation: PageStateValidation ): Result { try { const { expectedStep, requiredSelectors, forbiddenSelectors = [] } = validation; // Check required selectors are present const missingSelectors = requiredSelectors.filter(_selector => !actualState(_selector)); if (missingSelectors.length > 0) { const result: PageStateValidationResult = { isValid: false, message: `Page state mismatch: Expected to be on "${expectedStep}" page but missing required elements`, expectedStep, missingSelectors }; return Result.ok(result); } // Check forbidden selectors are absent const unexpectedSelectors = forbiddenSelectors.filter(_selector => actualState(_selector)); if (unexpectedSelectors.length > 0) { const result: PageStateValidationResult = { isValid: false, message: `Page state mismatch: Found unexpected elements on "${expectedStep}" page`, expectedStep, unexpectedSelectors }; return Result.ok(result); } // All checks passed const result: PageStateValidationResult = { isValid: true, message: `Page state valid for "${expectedStep}"`, expectedStep }; return Result.ok(result); } catch (error) { return Result.err( error instanceof Error ? error : new Error(`Page state validation failed: ${String(error)}`) ); } } /** * Enhanced validation that tries multiple selector strategies for real iRacing HTML. * This handles the mismatch between test expectations (data-indicator attributes) * and real HTML structure (Chakra UI components). * * @param actualState Function that checks if selectors exist on the page * @param validation Expected page state configuration * @param realMode Whether we're in real mode (using real HTML dumps) or mock mode * @returns Result with validation outcome */ validateStateEnhanced( actualState: (_selector: string) => boolean, validation: PageStateValidation, realMode: boolean = false ): Result { try { const { expectedStep, requiredSelectors, forbiddenSelectors = [] } = validation; // In real mode, try to match the actual HTML structure with fallbacks let selectorsToCheck = [...requiredSelectors]; if (realMode) { // Add fallback selectors for real iRacing HTML (Chakra UI structure) const fallbackMap: Record = { cars: [ '#set-cars', '[id*="cars"]', '.wizard-step[id*="cars"]', '.cars-panel', // Real iRacing fallbacks - use step container IDs '[data-testid*="set-cars"]', '.chakra-stack:has([data-testid*="cars"])', ], track: [ '#set-track', '[id*="track"]', '.wizard-step[id*="track"]', '.track-panel', // Real iRacing fallbacks '[data-testid*="set-track"]', '.chakra-stack:has([data-testid*="track"])', ], 'add-car': [ 'a.btn:has-text("Add a Car")', '.btn:has-text("Add a Car")', '[data-testid*="add-car"]', // Real iRacing button selectors 'a.btn.btn-primary.btn-block.btn-sm:has-text("Add a Car")', ], }; // For each required selector, add fallbacks const enhancedSelectors: string[] = []; for (const selector of requiredSelectors) { enhancedSelectors.push(selector); // Add step-specific fallbacks const lowerStep = expectedStep.toLowerCase(); if (fallbackMap[lowerStep]) { enhancedSelectors.push(...fallbackMap[lowerStep]); } // Generic Chakra UI fallbacks for wizard steps if (selector.includes('data-indicator')) { enhancedSelectors.push( `[id*="${expectedStep}"]`, `[data-testid*="${expectedStep}"]`, `.wizard-step:has([data-testid*="${expectedStep}"])`, ); } } selectorsToCheck = enhancedSelectors; } // Check required selectors are present (with fallbacks for real mode) const missingSelectors = requiredSelectors.filter(selector => { if (realMode) { const relatedSelectors = selectorsToCheck.filter(s => s.includes(expectedStep) || s.includes( selector .replace(/[\[\]"']/g, '') .replace('data-indicator=', ''), ), ); if (relatedSelectors.length === 0) { return !actualState(selector); } return !relatedSelectors.some(s => actualState(s)); } return !actualState(selector); }); if (missingSelectors.length > 0) { const result: PageStateValidationResult = { isValid: false, message: `Page state mismatch: Expected to be on "${expectedStep}" page but missing required elements`, expectedStep, missingSelectors }; return Result.ok(result); } // Check forbidden selectors are absent const unexpectedSelectors = forbiddenSelectors.filter(_selector => actualState(_selector)); if (unexpectedSelectors.length > 0) { const result: PageStateValidationResult = { isValid: false, message: `Page state mismatch: Found unexpected elements on "${expectedStep}" page`, expectedStep, unexpectedSelectors }; return Result.ok(result); } // All checks passed const result: PageStateValidationResult = { isValid: true, message: `Page state valid for "${expectedStep}"`, expectedStep }; return Result.ok(result); } catch (error) { return Result.err( error instanceof Error ? error : new Error(`Page state validation failed: ${String(error)}`) ); } } }