This commit is contained in:
2025-12-01 17:27:56 +01:00
parent e7ada8aa23
commit 98a09a3f2b
41 changed files with 2341 additions and 1525 deletions

View File

@@ -10,6 +10,7 @@ import type { PlaywrightConfig } from '../core/PlaywrightAutomationAdapter';
import { PlaywrightBrowserSession } from '../core/PlaywrightBrowserSession';
import { IRACING_SELECTORS, IRACING_TIMEOUTS } from './IRacingSelectors';
import { SafeClickService } from './SafeClickService';
import { getFixtureForStep } from '../engine/FixtureServer';
export class IRacingDomInteractor {
constructor(
@@ -953,28 +954,84 @@ export class IRacingDomInteractor {
async clickNewRaceInModal(): Promise<void> {
const page = this.getPage();
const modalSelector = IRACING_SELECTORS.hostedRacing.createRaceModal;
const newRaceSelector = IRACING_SELECTORS.hostedRacing.newRaceButton;
try {
this.log('info', 'Waiting for Create Race modal to appear');
const modalSelector = IRACING_SELECTORS.hostedRacing.createRaceModal;
const isFixtureHost =
this.isRealMode() &&
this.config.baseUrl &&
!this.config.baseUrl.includes('members.iracing.com');
if (isFixtureHost) {
try {
await page.waitForSelector(modalSelector, {
state: 'attached',
timeout: 3000,
});
} catch {
const fixture = getFixtureForStep(2);
if (fixture) {
const base = this.config.baseUrl.replace(/\/$/, '');
const url = `${base}/${fixture}`;
this.log('info', 'Fixture host detected, navigating directly to Step 2 fixture before New Race click', {
url,
});
await page.goto(url, {
waitUntil: 'domcontentloaded',
timeout: IRACING_TIMEOUTS.navigation,
});
}
}
}
await page.waitForSelector(modalSelector, {
state: 'attached',
timeout: IRACING_TIMEOUTS.elementWait,
});
this.log('info', 'Create Race modal attached, clicking New Race button');
const newRaceSelector = IRACING_SELECTORS.hostedRacing.newRaceButton;
this.log('info', 'Create Race modal attached, resolving New Race control', {
modalSelector,
newRaceSelector,
});
await page.waitForSelector(newRaceSelector, {
state: 'attached',
timeout: IRACING_TIMEOUTS.elementWait,
});
await this.safeClickService.safeClick(newRaceSelector, { timeout: IRACING_TIMEOUTS.elementWait });
this.log('info', 'Clicked New Race button, waiting for form to load');
await this.safeClickService.safeClick(newRaceSelector, {
timeout: IRACING_TIMEOUTS.elementWait,
});
this.log('info', 'Clicked New Race button, waiting for Race Information form to load');
await page.waitForTimeout(500);
if (isFixtureHost) {
const raceInfoFixture = getFixtureForStep(3);
if (raceInfoFixture) {
const base = this.config.baseUrl.replace(/\/$/, '');
const url = `${base}/${raceInfoFixture}`;
this.log(
'info',
'Fixture host detected, navigating directly to Step 3 Race Information fixture after New Race click',
{ url },
);
await page.goto(url, {
waitUntil: 'domcontentloaded',
timeout: IRACING_TIMEOUTS.navigation,
});
const raceInfoSelector =
IRACING_SELECTORS.wizard.stepContainers.raceInformation;
await page.waitForSelector(raceInfoSelector, {
state: 'attached',
timeout: IRACING_TIMEOUTS.elementWait,
});
}
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.log('error', 'Failed to click New Race in modal', { error: message });

View File

@@ -13,16 +13,31 @@ export const IRACING_SELECTORS = {
submitButton: 'button[type="submit"], button:has-text("Sign In")',
},
// Hosted Racing page (Step 2)
// Hosted Racing page (Step 1/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"]',
createRaceButton:
'button:has-text("Create a Race"), button[aria-label="Create a Race"], button.chakra-button:has-text("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")',
createRaceModal:
'#confirm-create-race-modal-modal-content, ' +
'#create-race-modal-modal-content, ' +
'#confirm-create-race-modal, ' +
'#create-race-modal, ' +
'#modal-children-container, ' +
'.modal-content',
newRaceButton:
'#confirm-create-race-modal-modal-content a.btn.btn-lg:has-text("New Race"), ' +
'#create-race-modal-modal-content a.btn.btn-lg:has-text("New Race"), ' +
'a.btn.btn-lg:has-text("New Race"), ' +
'a.btn.btn-info:has-text("New Race"), ' +
'.dropdown-menu a.dropdown-item.text-danger:has-text("New Race"), ' +
'.dropdown-menu a.dropdown-item:has-text("New Race"), ' +
'button.chakra-button:has-text("New Race")',
lastSettingsButton:
'#confirm-create-race-modal-modal-content a.btn.btn-lg:has-text("Last Settings"), ' +
'#create-race-modal-modal-content a.btn.btn-lg:has-text("Last Settings"), ' +
'a.btn.btn-lg:has-text("Last Settings"), ' +
'a.btn.btn-info:has-text("Last Settings")',
},
// Common modal/wizard selectors - VERIFIED from real HTML
@@ -31,28 +46,34 @@ export const IRACING_SELECTORS = {
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',
// Wizard footer buttons (fixture + live)
// Primary navigation uses sidebar; footer has Back/Next-style step links.
nextButton:
'.wizard-footer .btn-group.pull-xs-left a.btn.btn-sm:last-child, ' +
'.wizard-footer .btn-group a.btn.btn-sm:last-child, ' +
'.modal-footer .btn-toolbar a.btn:not(.dropdown-toggle), ' +
'.modal-footer .btn-group a.btn:last-child',
backButton:
'.wizard-footer .btn-group.pull-xs-left a.btn.btn-sm:first-child, ' +
'.wizard-footer .btn-group a.btn.btn-sm:first-child, ' +
'.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
// Wizard sidebar navigation links (use real sidebar IDs so text is present)
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"]',
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: {
@@ -121,14 +142,20 @@ export const IRACING_SELECTORS = {
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")',
carSearch:
'#select-car-set-cars input[placeholder*="Search"], ' +
'input[placeholder*="Search"]',
carList: '#select-car-set-cars table.table.table-striped, table.table.table-striped',
addCarButton:
'#select-car-set-cars a.btn.btn-primary:has-text("Add a Car"), ' +
'#select-car-set-cars a.btn.btn-primary:has-text("Add a Car 16 Available")',
addCarModal:
'#select-car-compact-content, ' +
'.drawer-container, ' +
'.drawer-container .drawer',
carSelectButton:
'#select-car-set-cars a.btn.btn-block:has-text("Select"), ' +
'a.btn.btn-block:has-text("Select")',
// Step 10/11/12: Track
trackSearch: 'input[placeholder*="Search"]',