Files
gridpilot.gg/packages/infrastructure/adapters/automation/IRacingSelectors.ts
2025-11-26 17:03:29 +01:00

212 lines
12 KiB
TypeScript

/**
* 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 real iRacing HTML captured 2024-11-23
*/
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: '[aria-label*="Hosted" i], [role="tab"]:has-text("Hosted")',
// Modal that appears after clicking "Create a Race"
createRaceModal: '#confirm-create-race-modal, .modal:has-text("Create a Race")',
// "New Race" button in the modal body (not footer) - two side-by-side buttons in a row
// Verified from real iRacing HTML: buttons are <a class="btn btn-lg btn-info btn-block"> in modal-body
newRaceButton: '#confirm-create-race-modal .modal-body a.btn:has-text("New Race"), #confirm-create-race-modal a.btn:has(.icon-wand)',
lastSettingsButton: '#confirm-create-race-modal .modal-body a.btn:has-text("Last Settings"), #confirm-create-race-modal a.btn:has(.icon-servers)',
},
// Common modal/wizard selectors - VERIFIED from real HTML
wizard: {
modal: '#create-race-modal, [role="dialog"], .modal.fade.in',
modalDialog: '#create-race-modal-modal-dialog, .modal-dialog',
modalContent: '#create-race-modal-modal-content, .modal-content',
modalTitle: '[data-testid="modal-title"], .modal-title',
// Wizard footer buttons - these are anchor tags styled as buttons
// The "Next" button shows the name of the next step (e.g., "Server Details")
nextButton: '.wizard-footer a.btn:not(.disabled):has(.icon-caret-right)',
backButton: '.wizard-footer a.btn:has(.icon-caret-left):has-text("Back")',
// Modal footer actions
confirmButton: '.modal-footer a.btn-success, button:has-text("Confirm"), button:has-text("OK")',
cancelButton: '.modal-footer a.btn-secondary:has-text("Back"), button:has-text("Cancel")',
closeButton: '.modal-header a.close, [aria-label="Close"]',
// Wizard sidebar navigation links - VERIFIED IDs from real HTML
sidebarLinks: {
raceInformation: '#wizard-sidebar-link-set-session-information',
serverDetails: '#wizard-sidebar-link-set-server-details',
admins: '#wizard-sidebar-link-set-admins',
timeLimit: '#wizard-sidebar-link-set-time-limit',
cars: '#wizard-sidebar-link-set-cars',
track: '#wizard-sidebar-link-set-track',
trackOptions: '#wizard-sidebar-link-set-track-options',
timeOfDay: '#wizard-sidebar-link-set-time-of-day',
weather: '#wizard-sidebar-link-set-weather',
raceOptions: '#wizard-sidebar-link-set-race-options',
trackConditions: '#wizard-sidebar-link-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: 'input.form-control, .chakra-input, input[type="text"]',
passwordInput: 'input[type="password"], input[maxlength="32"].form-control',
textarea: 'textarea.form-control, .chakra-textarea, textarea',
select: '.chakra-select, select.form-control, select',
checkbox: '.chakra-checkbox, input[type="checkbox"], .switch-checkbox',
slider: '.chakra-slider, input[type="range"]',
toggle: '.switch input.switch-checkbox, .toggle-switch input',
},
// Step-specific selectors - VERIFIED from real iRacing HTML structure
steps: {
// Step 3: Race Information - form structure inside #set-session-information
// Form groups have labels followed by inputs
sessionName: '#set-session-information .card-block .form-group:first-of-type input.form-control',
sessionNameAlt: '#set-session-information input.form-control[type="text"]:not([maxlength])',
password: '#set-session-information .card-block .form-group:nth-of-type(2) input.form-control',
passwordAlt: '#set-session-information input.form-control[maxlength="32"]',
description: '#set-session-information .card-block .form-group:last-of-type textarea.form-control',
descriptionAlt: '#set-session-information textarea.form-control',
// League racing toggle in Step 3
leagueRacingToggle: '#set-session-information .switch-checkbox',
// Step 4: Server Details
region: '#set-server-details select.form-control, #set-server-details [data-dropdown="region"]',
startNow: '#set-server-details .switch-checkbox, #set-server-details input[type="checkbox"]',
// Step 5/6: Admins
adminSearch: '.wizard-sidebar input[placeholder*="Search"], #set-admins input[placeholder*="Search"]',
adminList: '#set-admins [data-list="admins"]',
// Step 7: Time Limits - Bootstrap-slider uses hidden input[type="text"] with id containing slider name
// Also targets the visible slider handle for interaction
practice: '#set-time-limit input[id*="practice"], #set-time-limit .slider input[type="text"], #set-time-limit [data-slider="practice"]',
qualify: '#set-time-limit input[id*="qualify"], #set-time-limit .slider input[type="text"], #set-time-limit [data-slider="qualify"]',
race: '#set-time-limit input[id*="race"], #set-time-limit .slider input[type="text"], #set-time-limit [data-slider="race"]',
// Step 8/9: Cars
carSearch: '.wizard-sidebar input[placeholder*="Search"], #set-cars input[placeholder*="Search"], .modal input[placeholder*="Search"]',
carList: '#set-cars [data-list="cars"]',
// Add Car button - triggers car selection interface in wizard sidebar
// CORRECTED: Added fallback selectors since .icon-plus cannot be verified in minified HTML
addCarButton: '#set-cars a.btn:has(.icon-plus), #set-cars .card-header a.btn, #set-cars button:has-text("Add"), #set-cars a.btn:has-text("Add")',
// Car selection interface - CORRECTED: No separate modal, uses wizard sidebar within main modal
addCarModal: '#create-race-modal .wizard-sidebar, #set-cars .wizard-sidebar, .wizard-sidebar:has(input[placeholder*="Search"])',
// Select button inside car table row - clicking this adds the car immediately (no confirm step)
// The "Select" button is an anchor styled as: a.btn.btn-block.btn-primary.btn-xs
carSelectButton: '.wizard-sidebar table .btn-primary.btn-xs:has-text("Select"), #set-cars table .btn-primary.btn-xs:has-text("Select"), .modal table .btn-primary:has-text("Select")',
// Step 10/11/12: Track
trackSearch: '.wizard-sidebar input[placeholder*="Search"], #set-track input[placeholder*="Search"], .modal input[placeholder*="Search"]',
trackList: '#set-track [data-list="tracks"]',
// Add Track button - triggers track selection interface in wizard sidebar
// CORRECTED: Added fallback selectors since .icon-plus cannot be verified in minified HTML
addTrackButton: '#set-track a.btn:has(.icon-plus), #set-track .card-header a.btn, #set-track button:has-text("Add"), #set-track a.btn:has-text("Add")',
// Track selection interface - CORRECTED: No separate modal, uses wizard sidebar within main modal
addTrackModal: '#create-race-modal .wizard-sidebar, #set-track .wizard-sidebar, .wizard-sidebar:has(input[placeholder*="Search"])',
// Select button inside track table row - clicking this selects the track immediately (no confirm step)
// Prefer direct buttons (not dropdown toggles) for single-config tracks
trackSelectButton: '.wizard-sidebar table a.btn.btn-primary.btn-xs:not(.dropdown-toggle), #set-track table a.btn.btn-primary.btn-xs:not(.dropdown-toggle)',
// Dropdown toggle for multi-config tracks - opens a menu of track configurations
trackSelectDropdown: '.wizard-sidebar table a.btn.btn-primary.btn-xs.dropdown-toggle, #set-track table a.btn.btn-primary.btn-xs.dropdown-toggle',
// First item in the dropdown menu for selecting track configuration
trackSelectDropdownItem: '.dropdown-menu.show .dropdown-item:first-child, .dropdown-menu-lg .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: '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;