242 lines
8.1 KiB
TypeScript
242 lines
8.1 KiB
TypeScript
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<PageStateValidationInput, PageStateValidationResult, Error>
|
|
{
|
|
validate(input: PageStateValidationInput): Result<PageStateValidationResult, Error> {
|
|
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<PageStateValidationResult, Error> {
|
|
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<PageStateValidationResult, Error> {
|
|
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<string, string[]> = {
|
|
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)}`)
|
|
);
|
|
}
|
|
}
|
|
} |