/** * CSS Selector map for iRacing hosted session workflow. * Selectors are derived from HTML samples in resources/iracing-hosted-sessions/ * * The iRacing UI uses Chakra UI/React with dynamic CSS classes. * We prefer stable selectors: data-testid, id, aria-labels, role attributes. */ export interface StepSelectors { /** Primary container/step identifier */ container?: string; /** Wizard sidebar navigation link */ sidebarLink?: string; /** Wizard top navigation link */ wizardNav?: string; /** Form fields for this step */ fields?: Record; /** Buttons specific to this step */ buttons?: Record; /** Modal selectors if this is a modal step */ modal?: { container: string; closeButton: string; confirmButton?: string; searchInput?: string; resultsList?: string; selectButton?: string; }; } export interface IRacingSelectorMapType { /** Common selectors used across multiple steps */ common: { mainModal: string; modalDialog: string; modalContent: string; modalTitle: string; modalCloseButton: string; checkoutButton: string; backButton: string; nextButton: string; wizardContainer: string; wizardSidebar: string; searchInput: string; loadingSpinner: string; }; /** Step-specific selectors */ steps: Record; /** iRacing-specific URLs */ urls: { base: string; hostedRacing: string; login: string; }; } /** * Complete selector map for iRacing hosted session creation workflow. * * Steps: * 1. LOGIN - Login page (handled externally) * 2. HOSTED_RACING - Navigate to hosted racing section * 3. CREATE_RACE - Click create race button * 4. RACE_INFORMATION - Fill session name, password, description * 5. SERVER_DETAILS - Select server region, launch time * 6. SET_ADMINS - Admin configuration (modal at step 6) * 7. TIME_LIMITS - Configure time limits * 8. SET_CARS - Car selection overview * 9. ADD_CAR - Add a car (modal at step 9) * 10. SET_CAR_CLASSES - Configure car classes * 11. SET_TRACK - Track selection overview * 12. ADD_TRACK - Add a track (modal at step 12) * 13. TRACK_OPTIONS - Configure track options * 14. TIME_OF_DAY - Configure time of day * 15. WEATHER - Configure weather * 16. RACE_OPTIONS - Configure race options * 17. TEAM_DRIVING - Configure team driving * 18. TRACK_CONDITIONS - Final review (safety checkpoint - no final submit) */ export const IRacingSelectorMap: IRacingSelectorMapType = { common: { mainModal: '#create-race-modal', modalDialog: '#create-race-modal-modal-dialog', modalContent: '#create-race-modal-modal-content', modalTitle: '[data-testid="modal-title"]', modalCloseButton: '.modal-header .close, [data-testid="button-close-modal"]', checkoutButton: '.btn.btn-success', backButton: '.btn.btn-secondary:has(.icon-caret-left)', nextButton: '.btn.btn-secondary:has(.icon-caret-right)', wizardContainer: '#create-race-wizard', wizardSidebar: '.wizard-sidebar', searchInput: '.wizard-sidebar input[type="text"][placeholder="Search"]', loadingSpinner: '.loader-container .loader', }, urls: { base: 'https://members-ng.iracing.com', hostedRacing: 'https://members-ng.iracing.com/web/racing/hosted', login: 'https://members-ng.iracing.com/login', }, steps: { // Step 1: LOGIN - External, handled before automation 1: { container: '#login-form, .login-container', fields: { email: 'input[name="email"], #email', password: 'input[name="password"], #password', }, buttons: { submit: 'button[type="submit"], .login-button', }, }, // Step 2: HOSTED_RACING - Navigate to hosted racing page 2: { container: '#hosted-sessions, [data-page="hosted"]', sidebarLink: 'a[href*="/racing/hosted"]', buttons: { createRace: '.btn:has-text("Create a Race"), [data-action="create-race"]', }, }, // Step 3: CREATE_RACE - Click create race to open modal 3: { container: '[data-modal-component="ModalCreateRace"]', buttons: { createRace: 'button:has-text("Create a Race"), .btn-primary:has-text("Create")', }, }, // Step 4: RACE_INFORMATION - Fill session name, password, description 4: { container: '#set-session-information', sidebarLink: '#wizard-sidebar-link-set-session-information', wizardNav: '[data-testid="wizard-nav-set-session-information"]', fields: { sessionName: '.form-group:has(label:has-text("Session Name")) input, input[name="sessionName"]', password: '.form-group:has(label:has-text("Password")) input, input[name="password"]', description: '.form-group:has(label:has-text("Description")) textarea, textarea[name="description"]', }, buttons: { next: '.wizard-footer .btn:has-text("Server Details")', }, }, // Step 5: SERVER_DETAILS - Select server region and launch time 5: { container: '#set-server-details', sidebarLink: '#wizard-sidebar-link-set-server-details', wizardNav: '[data-testid="wizard-nav-set-server-details"]', fields: { serverRegion: '.chakra-accordion__button[data-index="0"]', launchTime: 'input[name="launchTime"], [id*="field-"]:has(+ [placeholder="Now"])', startNow: '.switch:has(input[value="startNow"])', }, buttons: { next: '.wizard-footer .btn:has-text("Admins")', back: '.wizard-footer .btn:has-text("Race Information")', }, }, // Step 6: SET_ADMINS - Admin configuration (modal step) 6: { container: '#set-admins', sidebarLink: '#wizard-sidebar-link-set-admins', wizardNav: '[data-testid="wizard-nav-set-admins"]', buttons: { addAdmin: '.btn:has-text("Add Admin"), .btn-primary:has(.icon-add)', next: '.wizard-footer .btn:has-text("Time Limit")', back: '.wizard-footer .btn:has-text("Server Details")', }, modal: { container: '#add-admin-modal, .modal:has([data-modal-component="AddAdmin"])', closeButton: '.modal .close, [data-testid="button-close-modal"]', searchInput: 'input[placeholder*="Search"], input[name="adminSearch"]', resultsList: '.admin-list, .search-results', selectButton: '.btn:has-text("Select"), .btn-primary:has-text("Add")', }, }, // Step 7: TIME_LIMITS - Configure time limits 7: { container: '#set-time-limit', sidebarLink: '#wizard-sidebar-link-set-time-limit', wizardNav: '[data-testid="wizard-nav-set-time-limit"]', fields: { practiceLength: 'input[name="practiceLength"]', qualifyLength: 'input[name="qualifyLength"]', raceLength: 'input[name="raceLength"]', warmupLength: 'input[name="warmupLength"]', }, buttons: { next: '.wizard-footer .btn:has-text("Cars")', back: '.wizard-footer .btn:has-text("Admins")', }, }, // Step 8: SET_CARS - Car selection overview 8: { container: '#set-cars', sidebarLink: '#wizard-sidebar-link-set-cars', wizardNav: '[data-testid="wizard-nav-set-cars"]', buttons: { addCar: '.btn:has-text("Add Car"), .btn-primary:has(.icon-add)', next: '.wizard-footer .btn:has-text("Track")', back: '.wizard-footer .btn:has-text("Time Limit")', }, }, // Step 9: ADD_CAR - Add a car (modal step) 9: { container: '#set-cars', sidebarLink: '#wizard-sidebar-link-set-cars', wizardNav: '[data-testid="wizard-nav-set-cars"]', modal: { container: '#add-car-modal, .modal:has(.car-list)', closeButton: '.modal .close, [aria-label="Close"]', searchInput: 'input[placeholder*="Search"], .car-search input', resultsList: '.car-list table tbody, .car-grid', selectButton: '.btn:has-text("Select"), .btn-primary.btn-xs:has-text("Select")', }, }, // Step 10: SET_CAR_CLASSES - Configure car classes 10: { container: '#set-car-classes, #set-cars', sidebarLink: '#wizard-sidebar-link-set-cars', wizardNav: '[data-testid="wizard-nav-set-cars"]', fields: { carClass: 'select[name="carClass"], .car-class-select', }, buttons: { next: '.wizard-footer .btn:has-text("Track")', }, }, // Step 11: SET_TRACK - Track selection overview 11: { container: '#set-track', sidebarLink: '#wizard-sidebar-link-set-track', wizardNav: '[data-testid="wizard-nav-set-track"]', buttons: { addTrack: '.btn:has-text("Add Track"), .btn-primary:has(.icon-add)', next: '.wizard-footer .btn:has-text("Track Options")', back: '.wizard-footer .btn:has-text("Cars")', }, }, // Step 12: ADD_TRACK - Add a track (modal step) 12: { container: '#set-track', sidebarLink: '#wizard-sidebar-link-set-track', wizardNav: '[data-testid="wizard-nav-set-track"]', modal: { container: '#add-track-modal, .modal:has(.track-list)', closeButton: '.modal .close, [aria-label="Close"]', searchInput: 'input[placeholder*="Search"], .track-search input', resultsList: '.track-list table tbody, .track-grid', selectButton: '.btn:has-text("Select"), .btn-primary.btn-xs:has-text("Select")', }, }, // Step 13: TRACK_OPTIONS - Configure track options 13: { container: '#set-track-options', sidebarLink: '#wizard-sidebar-link-set-track-options', wizardNav: '[data-testid="wizard-nav-set-track-options"]', fields: { trackConfig: 'select[name="trackConfig"]', pitStalls: 'input[name="pitStalls"]', }, buttons: { next: '.wizard-footer .btn:has-text("Time of Day")', back: '.wizard-footer .btn:has-text("Track")', }, }, // Step 14: TIME_OF_DAY - Configure time of day 14: { container: '#set-time-of-day', sidebarLink: '#wizard-sidebar-link-set-time-of-day', wizardNav: '[data-testid="wizard-nav-set-time-of-day"]', fields: { timeOfDay: 'input[name="timeOfDay"], .time-slider', date: 'input[name="date"], .date-picker', }, buttons: { next: '.wizard-footer .btn:has-text("Weather")', back: '.wizard-footer .btn:has-text("Track Options")', }, }, // Step 15: WEATHER - Configure weather 15: { container: '#set-weather', sidebarLink: '#wizard-sidebar-link-set-weather', wizardNav: '[data-testid="wizard-nav-set-weather"]', fields: { weatherType: 'select[name="weatherType"]', temperature: 'input[name="temperature"]', humidity: 'input[name="humidity"]', windSpeed: 'input[name="windSpeed"]', windDirection: 'input[name="windDirection"]', }, buttons: { next: '.wizard-footer .btn:has-text("Race Options")', back: '.wizard-footer .btn:has-text("Time of Day")', }, }, // Step 16: RACE_OPTIONS - Configure race options 16: { container: '#set-race-options', sidebarLink: '#wizard-sidebar-link-set-race-options', wizardNav: '[data-testid="wizard-nav-set-race-options"]', fields: { maxDrivers: 'input[name="maxDrivers"]', hardcoreIncidents: '.switch:has(input[name="hardcoreIncidents"])', rollingStarts: '.switch:has(input[name="rollingStarts"])', fullCourseCautions: '.switch:has(input[name="fullCourseCautions"])', }, buttons: { next: '.wizard-footer .btn:has-text("Track Conditions")', back: '.wizard-footer .btn:has-text("Weather")', }, }, // Step 17: TEAM_DRIVING - Configure team driving (if applicable) 17: { container: '#set-team-driving', fields: { teamDriving: '.switch:has(input[name="teamDriving"])', minDrivers: 'input[name="minDrivers"]', maxDrivers: 'input[name="maxDrivers"]', }, buttons: { next: '.wizard-footer .btn:has-text("Track Conditions")', back: '.wizard-footer .btn:has-text("Race Options")', }, }, // Step 18: TRACK_CONDITIONS - Final review (safety checkpoint - NO final submit) 18: { container: '#set-track-conditions', sidebarLink: '#wizard-sidebar-link-set-track-conditions', wizardNav: '[data-testid="wizard-nav-set-track-conditions"]', fields: { trackState: 'select[name="trackState"]', marbles: '.switch:has(input[name="marbles"])', rubberedTrack: '.switch:has(input[name="rubberedTrack"])', }, buttons: { // NOTE: Checkout button is intentionally NOT included for safety // The automation should stop here and let the user review/confirm manually back: '.wizard-footer .btn:has-text("Race Options")', }, }, }, }; /** * Get selectors for a specific step */ export function getStepSelectors(stepId: number): StepSelectors | undefined { return IRacingSelectorMap.steps[stepId]; } /** * Check if a step is a modal step (requires opening a secondary dialog) */ export function isModalStep(stepId: number): boolean { return stepId === 6 || stepId === 9 || stepId === 12; } /** * Get the step name for logging/debugging */ export function getStepName(stepId: number): string { const stepNames: Record = { 1: 'LOGIN', 2: 'HOSTED_RACING', 3: 'CREATE_RACE', 4: 'RACE_INFORMATION', 5: 'SERVER_DETAILS', 6: 'SET_ADMINS', 7: 'TIME_LIMITS', 8: 'SET_CARS', 9: 'ADD_CAR', 10: 'SET_CAR_CLASSES', 11: 'SET_TRACK', 12: 'ADD_TRACK', 13: 'TRACK_OPTIONS', 14: 'TIME_OF_DAY', 15: 'WEATHER', 16: 'RACE_OPTIONS', 17: 'TEAM_DRIVING', 18: 'TRACK_CONDITIONS', }; return stepNames[stepId] || `UNKNOWN_STEP_${stepId}`; }