wip
This commit is contained in:
224
packages/automation/domain/services/PageStateValidator.ts
Normal file
224
packages/automation/domain/services/PageStateValidator.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import { Result } from '../shared/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[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
/**
|
||||
* 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)}`)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { StepId } from '../value-objects/StepId';
|
||||
import { SessionState } from '../value-objects/SessionState';
|
||||
|
||||
export interface ValidationResult {
|
||||
isValid: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const STEP_DESCRIPTIONS: Record<number, string> = {
|
||||
1: 'Navigate to Hosted Racing page',
|
||||
2: 'Click Create a Race',
|
||||
3: 'Fill Race Information',
|
||||
4: 'Configure Server Details',
|
||||
5: 'Set Admins',
|
||||
6: 'Add Admin (Modal)',
|
||||
7: 'Set Time Limits',
|
||||
8: 'Set Cars',
|
||||
9: 'Add a Car (Modal)',
|
||||
10: 'Set Car Classes',
|
||||
11: 'Set Track',
|
||||
12: 'Add a Track (Modal)',
|
||||
13: 'Configure Track Options',
|
||||
14: 'Set Time of Day',
|
||||
15: 'Configure Weather',
|
||||
16: 'Set Race Options',
|
||||
17: 'Track Conditions (STOP - Manual Submit Required)',
|
||||
};
|
||||
|
||||
export class StepTransitionValidator {
|
||||
static canTransition(
|
||||
currentStep: StepId,
|
||||
nextStep: StepId,
|
||||
state: SessionState
|
||||
): ValidationResult {
|
||||
if (!state.isInProgress()) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'Session must be in progress to transition steps',
|
||||
};
|
||||
}
|
||||
|
||||
if (currentStep.equals(nextStep)) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'Already at this step',
|
||||
};
|
||||
}
|
||||
|
||||
if (nextStep.value < currentStep.value) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'Cannot move backward - steps must progress forward only',
|
||||
};
|
||||
}
|
||||
|
||||
if (nextStep.value !== currentStep.value + 1) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'Cannot skip steps - must progress sequentially',
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
static validateModalStepTransition(
|
||||
currentStep: StepId,
|
||||
nextStep: StepId
|
||||
): ValidationResult {
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
static shouldStopAtStep18(nextStep: StepId): boolean {
|
||||
return nextStep.isFinalStep();
|
||||
}
|
||||
|
||||
static getStepDescription(step: StepId): string {
|
||||
return STEP_DESCRIPTIONS[step.value] || `Step ${step.value}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user