212 lines
12 KiB
TypeScript
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: '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-content, .modal-content',
|
|
modalDialog: '.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")
|
|
// In the dumps, the footer has two buttons: Previous Step (left) and Next Step (right)
|
|
nextButton: '.wizard-footer a.btn:last-child',
|
|
backButton: '.wizard-footer a.btn:first-child',
|
|
// 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: '[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: 'input.form-control, .chakra-input, 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 - 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, #set-session-information [data-field="sessionName"], [data-field="sessionName"]',
|
|
sessionNameAlt: '#set-session-information input.form-control[type="text"]:not([maxlength]), input[data-field="sessionName"]',
|
|
password: '#set-session-information .card-block .form-group:nth-of-type(2) input.form-control, #set-session-information input[type="password"], #set-session-information input.chakra-input[type="text"]:not([name="Current page"]):not([id*="field-:rue:"]):not([id*="field-:rug:"]):not([id*="field-:ruj:"]):not([id*="field-:rl5b:"]):not([id*="field-:rktk:"]), #set-session-information [data-field="password"], [data-field="password"]',
|
|
passwordAlt: '#set-session-information input.form-control[maxlength="32"], input[data-field="password"]',
|
|
description: '#set-session-information .card-block .form-group:last-of-type textarea.form-control, #set-session-information textarea[data-field="description"], [data-field="description"]',
|
|
descriptionAlt: '#set-session-information textarea.form-control, textarea[data-field="description"]',
|
|
// 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"]',
|
|
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: '[data-list="admins"]', // Keep generic if not found in dumps, but search input is verified
|
|
addAdminButton: 'a.btn:has-text("Add an Admin")',
|
|
|
|
// Step 7: Time Limits - Bootstrap-slider uses hidden input[type="text"] with id containing slider name
|
|
// Also targets the visible slider handle for interaction
|
|
// Dumps show dynamic IDs like time-limit-slider1763726367635
|
|
practice: 'input[id*="time-limit-slider"]', // This is risky if multiple sliders share the same ID pattern.
|
|
// TODO: Need better selectors for specific sliders if they exist.
|
|
// For now, we'll assume the automation handles finding the right one by index or label if possible.
|
|
qualify: 'input[id*="qualify"], input[id*="time-limit-slider"]',
|
|
race: 'input[id*="race"], input[id*="time-limit-slider"]',
|
|
|
|
// Step 8/9: Cars
|
|
carSearch: 'input[placeholder*="Search"]',
|
|
carList: 'table.table.table-striped',
|
|
// Add Car button - triggers car selection interface in wizard sidebar
|
|
addCarButton: 'a.btn:has-text("Add a Car")',
|
|
// Car selection interface - CORRECTED: No separate modal, uses wizard sidebar within main modal
|
|
addCarModal: '.wizard-sidebar',
|
|
// Select button inside car table row - clicking this adds the car immediately (no confirm step)
|
|
carSelectButton: 'a.btn:has-text("Select")',
|
|
|
|
// Step 10/11/12: Track
|
|
trackSearch: 'input[placeholder*="Search"]',
|
|
trackList: 'table.table.table-striped',
|
|
// Add Track button - triggers track selection interface in wizard sidebar
|
|
addTrackButton: 'a.btn:has-text("Add a Track")',
|
|
// Track selection interface - CORRECTED: No separate modal, uses wizard sidebar within main modal
|
|
addTrackModal: '.wizard-sidebar',
|
|
// Select button inside track table row - clicking this selects the track immediately (no confirm step)
|
|
trackSelectButton: 'a.btn:has-text("Select")',
|
|
// 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; |