399 lines
14 KiB
TypeScript
399 lines
14 KiB
TypeScript
/**
|
|
* 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<string, string>;
|
|
/** Buttons specific to this step */
|
|
buttons?: Record<string, string>;
|
|
/** 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<number, StepSelectors>;
|
|
/** 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<number, string> = {
|
|
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}`;
|
|
} |