/** * Selectors for the real iRacing website (members.iracing.com) * Uses text-based and ARIA selectors since the site uses React/Chakra UI * with dynamically generated class names. * * VERIFIED against html-dumps-optimized 2025-11-27 */ export const IRACING_SELECTORS = { // Login page login: { emailInput: '#username, input[name="username"], input[type="email"]', passwordInput: '#password, input[type="password"]', submitButton: 'button[type="submit"], button:has-text("Sign In")', }, // Hosted Racing page (Step 2) hostedRacing: { // Main "Create a Race" button on the hosted sessions page createRaceButton: 'button:has-text("Create a Race"), button[aria-label="Create a Race"]', hostedTab: 'a:has-text("Hosted")', // Modal that appears after clicking "Create a Race" createRaceModal: '#modal-children-container, .modal-content', // "New Race" button in the modal body (not footer) - two side-by-side buttons in a row newRaceButton: 'a.btn:has-text("New Race")', lastSettingsButton: 'a.btn:has-text("Last Settings")', }, // Common modal/wizard selectors - VERIFIED from real HTML wizard: { modal: '#create-race-modal, .modal.fade.in', modalDialog: '#create-race-modal-modal-dialog, .modal-dialog', modalContent: '#create-race-modal-modal-content, .modal-content', modalTitle: '[data-testid="modal-title"]', // Wizard footer buttons - CORRECTED: The footer contains navigation buttons and dropup menus // The main navigation is via the sidebar links, footer has Back/Next style buttons // Based on dumps, footer has .btn-group with buttons for navigation nextButton: '.modal-footer .btn-toolbar a.btn:not(.dropdown-toggle), .modal-footer .btn-group a.btn:last-child', backButton: '.modal-footer .btn-group a.btn:first-child', // Modal footer actions confirmButton: '.modal-footer a.btn-success, .modal-footer button:has-text("Confirm"), button:has-text("OK")', cancelButton: '.modal-footer a.btn-secondary, button:has-text("Cancel")', closeButton: '[data-testid="button-close-modal"]', // Wizard sidebar navigation links - VERIFIED from dumps sidebarLinks: { raceInformation: '[data-testid="wizard-nav-set-session-information"]', serverDetails: '[data-testid="wizard-nav-set-server-details"]', admins: '[data-testid="wizard-nav-set-admins"]', timeLimit: '[data-testid="wizard-nav-set-time-limit"]', cars: '[data-testid="wizard-nav-set-cars"]', track: '[data-testid="wizard-nav-set-track"]', trackOptions: '[data-testid="wizard-nav-set-track-options"]', timeOfDay: '[data-testid="wizard-nav-set-time-of-day"]', weather: '[data-testid="wizard-nav-set-weather"]', raceOptions: '[data-testid="wizard-nav-set-race-options"]', trackConditions: '[data-testid="wizard-nav-set-track-conditions"]', }, // Wizard step containers (the visible step content) stepContainers: { raceInformation: '#set-session-information', serverDetails: '#set-server-details', admins: '#set-admins', timeLimit: '#set-time-limit', cars: '#set-cars', track: '#set-track', trackOptions: '#set-track-options', timeOfDay: '#set-time-of-day', weather: '#set-weather', raceOptions: '#set-race-options', trackConditions: '#set-track-conditions', }, }, // Form fields - based on actual iRacing DOM structure fields: { textInput: '.chakra-input, input.form-control, input[type="text"], input[data-field], input[data-test], input[placeholder]', passwordInput: 'input[type="password"], input[maxlength="32"].form-control, input[data-field="password"], input[name="password"]', textarea: 'textarea.form-control, .chakra-textarea, textarea, textarea[data-field]', select: '.chakra-select, select.form-control, select, [data-dropdown], select[data-field]', checkbox: '.chakra-checkbox, input[type="checkbox"], .switch-checkbox, input[data-toggle], [data-toggle]', slider: '.chakra-slider, .slider, input[type="range"]', toggle: '.switch input.switch-checkbox, .toggle-switch input, input[data-toggle]', }, // Step-specific selectors - VERIFIED from real iRacing HTML structure steps: { // Step 3: Race Information - CORRECTED based on actual HTML structure // Session name is a text input in a form-group with label "Session Name" sessionName: '#set-session-information input.form-control[type="text"]:not([maxlength])', sessionNameAlt: 'input[name="sessionName"], input.form-control[type="text"]', // Password field has maxlength="32" and is a text input (not type="password") password: '#set-session-information input.form-control[maxlength="32"]', passwordAlt: 'input[maxlength="32"][type="text"]', // Description is a textarea in the form description: '#set-session-information textarea.form-control', descriptionAlt: 'textarea.form-control', // League racing toggle in Step 3 leagueRacingToggle: '#set-session-information .switch-checkbox, [data-toggle="leagueRacing"]', // Step 4: Server Details region: '#set-server-details select.form-control, ' + '#set-server-details [data-dropdown="region"], ' + '#set-server-details [data-dropdown], ' + '[data-dropdown="region"], ' + '#set-server-details [role="radiogroup"] input[type="radio"]', startNow: '#set-server-details .switch-checkbox, ' + '#set-server-details input[type="checkbox"], ' + '[data-toggle="startNow"], ' + 'input[data-toggle="startNow"]', // Step 5/6: Admins adminSearch: 'input[placeholder*="Search"]', adminList: '#set-admins table.table.table-striped, #set-admins .card-block table', addAdminButton: 'a.btn:has-text("Add an Admin")', // Step 7: Time Limits - Bootstrap-slider uses hidden input[type="text"] with dynamic id // Fixtures show ids like time-limit-slider1764248520320 practice: '#set-time-limit input[id*="time-limit-slider"]', qualify: '#set-time-limit input[id*="time-limit-slider"]', race: '#set-time-limit input[id*="time-limit-slider"]', // Step 8/9: Cars carSearch: 'input[placeholder*="Search"]', carList: 'table.table.table-striped', // Add Car button - CORRECTED: Uses specific class and text addCarButton: 'a.btn.btn-primary.btn-block.btn-sm:has-text("Add a Car")', // Car selection interface - drawer that opens within the wizard sidebar addCarModal: '.drawer-container .drawer', // Select button inside car dropdown - opens config selection carSelectButton: 'a.btn.btn-primary.btn-xs.dropdown-toggle:has-text("Select")', // Step 10/11/12: Track trackSearch: 'input[placeholder*="Search"]', trackList: 'table.table.table-striped', // Add Track button - CORRECTED: Uses specific class and text addTrackButton: 'a.btn.btn-primary.btn-block.btn-sm:has-text("Add a Track")', // Track selection interface - drawer that opens within the card addTrackModal: '.drawer-container .drawer', // Select button inside track dropdown - opens config selection trackSelectButton: 'a.btn.btn-primary.btn-xs.dropdown-toggle:has-text("Select")', // Dropdown toggle for multi-config tracks - opens a menu of track configurations trackSelectDropdown: 'a.btn.btn-primary.btn-xs.dropdown-toggle', // First item in the dropdown menu for selecting track configuration trackSelectDropdownItem: '.dropdown-menu.dropdown-menu-right .dropdown-item:first-child', // Step 13: Track Options trackConfig: '#set-track-options select.form-control, #set-track-options [data-dropdown="trackConfig"]', // Step 14: Time of Day - iRacing uses datetime picker (rdt class) and Bootstrap-slider components // The datetime picker has input.form-control, sliders have hidden input[type="text"] timeOfDay: '#set-time-of-day .rdt input.form-control, #set-time-of-day input[id*="slider"], #set-time-of-day .slider input[type="text"], #set-time-of-day [data-slider="timeOfDay"]', // Step 15: Weather weatherType: '#set-weather select.form-control, #set-weather [data-dropdown="weatherType"]', // Temperature slider uses Bootstrap-slider with hidden input[type="text"] temperature: '#set-weather input[id*="slider"], #set-weather .slider input[type="text"], #set-weather [data-slider="temperature"]', // Step 16: Race Options maxDrivers: '#set-race-options input[name*="maxDrivers"], #set-race-options input[type="number"]', rollingStart: '#set-race-options .switch-checkbox[name*="rolling"], #set-race-options input[type="checkbox"]', // Step 17: Track Conditions (final step) trackState: '#set-track-conditions select.form-control, #set-track-conditions [data-dropdown="trackState"]', }, /** * DANGER ZONE - Selectors for checkout/payment buttons that should NEVER be clicked. * The automation must block any click on these selectors to prevent accidental purchases. * VERIFIED from real iRacing HTML - the checkout button has class btn-success with icon-cart */ BLOCKED_SELECTORS: { // Checkout/payment buttons - NEVER click these (verified from real HTML) checkout: '.chakra-button:has-text("Check Out"), a.btn-success:has(.icon-cart), a.btn:has-text("Check Out"), button:has-text("Check Out"), [data-testid*="checkout"]', purchase: 'button:has-text("Purchase"), a.btn:has-text("Purchase"), .chakra-button:has-text("Purchase"), button[aria-label="Purchase"]', buy: 'button:has-text("Buy"), a.btn:has-text("Buy Now"), button:has-text("Buy Now")', payment: 'button[type="submit"]:has-text("Submit Payment"), .payment-button, #checkout-button, button:has-text("Pay"), a.btn:has-text("Pay")', cart: 'a.btn:has(.icon-cart), button:has(.icon-cart), .btn-success:has(.icon-cart)', // Price labels that indicate purchase actions (e.g., "$0.50") priceAction: 'a.btn:has(.label-pill:has-text("$")), button:has(.label-pill:has-text("$")), .btn:has(.label-inverse:has-text("$"))', }, } as const; /** * Combined selector for all blocked/dangerous elements. * Use this to check if any selector targets a payment button. */ export const ALL_BLOCKED_SELECTORS = Object.values(IRACING_SELECTORS.BLOCKED_SELECTORS).join(', '); /** * Keywords that indicate a dangerous/checkout action. * Used for text-based safety checks. */ export const BLOCKED_KEYWORDS = [ 'checkout', 'check out', 'purchase', 'buy now', 'buy', 'pay', 'submit payment', 'add to cart', 'proceed to payment', ] as const; export const IRACING_URLS = { hostedSessions: 'https://members-ng.iracing.com/web/racing/hosted/browse-sessions', login: 'https://members.iracing.com/membersite/login.jsp', home: 'https://members.iracing.com', } as const; /** * Timeout values for real iRacing automation (in milliseconds) */ export const IRACING_TIMEOUTS = { navigation: 30000, elementWait: 15000, loginWait: 120000, // 2 minutes for manual login pageLoad: 20000, } as const;