diff --git a/packages/infrastructure/adapters/automation/PlaywrightAutomationAdapter.ts b/packages/infrastructure/adapters/automation/PlaywrightAutomationAdapter.ts index af99a221d..50bdccf46 100644 --- a/packages/infrastructure/adapters/automation/PlaywrightAutomationAdapter.ts +++ b/packages/infrastructure/adapters/automation/PlaywrightAutomationAdapter.ts @@ -1062,25 +1062,26 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent } // Robust: try opening Cars via sidebar nav then wait for a set of fallback selectors. - const carsFallbackSelector = '#set-cars, .wizard-step[id*="cars"], .cars-panel'; + const carsFallbackSelector = '#set-cars, #select-car-compact-content, .cars-panel, [id*="select-car"], [data-step="set-cars"]'; try { - try { - await this.page!.click('[data-testid="wizard-nav-set-cars"]'); - this.log('debug', 'Clicked wizard nav for Cars', { selector: '[data-testid="wizard-nav-set-cars"]' }); - } catch (e) { - this.log('debug', 'Wizard nav for Cars not present (continuing)', { error: String(e) }); - } + this.log('debug', 'nav-click attempted for Cars', { navSelector: '[data-testid="wizard-nav-set-cars"]' }); + // Attempt nav click (best-effort) - tolerate absence + await this.page!.click('[data-testid="wizard-nav-set-cars"]').catch(() => {}); + this.log('debug', 'Primary nav-click attempted', { selector: '[data-testid="wizard-nav-set-cars"]' }); try { + this.log('debug', 'Waiting for Cars panel using primary selector', { selector: carsFallbackSelector }); await this.page!.waitForSelector(carsFallbackSelector, { state: 'attached', timeout: 5000 }); this.log('info', 'Cars panel found', { selector: carsFallbackSelector }); } catch (err) { - this.log('warn', 'Cars panel not found with fallback selector, dumping #create-race-wizard innerHTML', { selector: carsFallbackSelector }); - const inner = await this.page!.evaluate(() => document.querySelector('#create-race-wizard')?.innerHTML || ''); - this.log('debug', 'create-race-wizard innerHTML (truncated)', { html: inner ? inner.substring(0, 2000) : '' }); - // Retry nav click once then wait longer before failing - try { await this.page!.click('[data-testid="wizard-nav-set-cars"]'); } catch {} + this.log('warn', 'Cars panel not found with primary selector, capturing wizard HTML and retrying', { selector: carsFallbackSelector }); + const html = await this.page!.innerHTML('#create-race-wizard').catch(() => ''); + this.log('debug', 'captured #create-race-wizard innerHTML (truncated)', { html: html ? html.slice(0, 2000) : '' }); + this.log('info', 'retry attempted for Cars nav-click', { attempt: 1 }); + // Retry nav click once (best-effort) then wait longer before failing + await this.page!.click('[data-testid="wizard-nav-set-cars"]').catch(() => {}); await this.page!.waitForSelector(carsFallbackSelector, { state: 'attached', timeout: 10000 }); + this.log('info', 'Cars panel found after retry', { selector: carsFallbackSelector }); } } catch (e) { this.log('error', 'Failed waiting for Cars panel', { error: String(e), selector: carsFallbackSelector }); @@ -2374,19 +2375,33 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent } try { - this.log('debug', 'Waiting for Add Car modal to appear'); - // Wait for modal container - use 'attached' because iRacing wizard steps have class="hidden" - const modalSelector = IRACING_SELECTORS.steps.addCarModal; + this.log('debug', 'Waiting for Add Car modal to appear (primary selector)'); + // Wait for modal container - expanded selector list to tolerate UI variants + const modalSelector = '#add-car-modal, #select-car-compact-content, .drawer[id*="select-car"], [id*="select-car-compact"], .select-car-modal'; await this.page.waitForSelector(modalSelector, { state: 'attached', timeout: this.isRealMode() ? IRACING_TIMEOUTS.elementWait : this.config.timeout, }); // Brief pause for modal animation (reduced from 300ms) await this.page.waitForTimeout(150); - this.log('info', 'Add Car modal is visible'); + this.log('info', 'Add Car modal is visible', { selector: modalSelector }); } catch (error) { const message = error instanceof Error ? error.message : String(error); - this.log('warn', 'Add Car modal did not appear', { error: message }); + this.log('warn', 'Add Car modal not found with primary selector, dumping #create-race-wizard innerHTML and retrying', { error: message }); + const html = await this.page!.innerHTML('#create-race-wizard').catch(() => ''); + this.log('debug', 'create-race-wizard innerHTML (truncated)', { html: html ? html.slice(0,2000) : '' }); + this.log('info', 'Retrying wait for Add Car modal with extended timeout'); + try { + const modalSelectorRetry = '#add-car-modal, #select-car-compact-content, .drawer[id*="select-car"], [id*="select-car-compact"], .select-car-modal'; + await this.page.waitForSelector(modalSelectorRetry, { + state: 'attached', + timeout: 10000, + }); + await this.page.waitForTimeout(150); + this.log('info', 'Add Car modal found after retry', { selector: modalSelectorRetry }); + } catch { + this.log('warn', 'Add Car modal still not found after retry'); + } // Don't throw - modal might appear differently in real iRacing } } @@ -2463,35 +2478,56 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent // First try direct select button (non-dropdown) const directSelector = '.modal table a.btn.btn-primary.btn-xs:not(.dropdown-toggle)'; const directButton = this.page.locator(directSelector).first(); - + if (await directButton.count() > 0 && await directButton.isVisible()) { await this.safeClick(directSelector, { timeout: IRACING_TIMEOUTS.elementWait }); - this.log('info', 'Clicked direct Select button for first search result'); + this.log('info', 'Clicked direct Select button for first search result', { selector: directSelector }); return; } - + // Fallback: dropdown toggle pattern const dropdownSelector = '.modal table a.btn.btn-primary.btn-xs.dropdown-toggle'; const dropdownButton = this.page.locator(dropdownSelector).first(); - + if (await dropdownButton.count() > 0 && await dropdownButton.isVisible()) { // Click dropdown to open menu await this.safeClick(dropdownSelector, { timeout: IRACING_TIMEOUTS.elementWait }); - this.log('debug', 'Clicked dropdown toggle, waiting for menu'); - + this.log('debug', 'Clicked dropdown toggle, waiting for menu', { selector: dropdownSelector }); + // Wait for dropdown menu to appear await this.page.waitForSelector('.dropdown-menu.show', { timeout: 3000 }).catch(() => {}); - + // Click first item in dropdown (first track config) const itemSelector = '.dropdown-menu.show .dropdown-item:first-child'; await this.page.waitForTimeout(200); await this.safeClick(itemSelector, { timeout: IRACING_TIMEOUTS.elementWait }); - this.log('info', 'Clicked first dropdown item to select track config'); + this.log('info', 'Clicked first dropdown item to select track config', { selector: itemSelector }); return; } - - // If neither found, throw error - throw new Error('No Select button found in modal table'); + + // Final fallback: try tolerant car row selectors (various UI variants) + const carRowSelector = '.car-row, .car-item, [data-testid*="car"], [id*="favorite_cars"], [id*="select-car"]'; + const carRow = this.page.locator(carRowSelector).first(); + if (await carRow.count() > 0) { + this.log('info', 'Fallback: clicking car row/item to select', { selector: carRowSelector }); + // Click the row itself (or its first clickable descendant) + try { + await this.safeClick(carRowSelector, { timeout: IRACING_TIMEOUTS.elementWait }); + this.log('info', 'Clicked car row fallback selector'); + return; + } catch (e) { + this.log('debug', 'Car row fallback click failed, attempting to click first link inside row', { error: String(e) }); + const linkInside = this.page.locator(`${carRowSelector} a, ${carRowSelector} button`).first(); + if (await linkInside.count() > 0 && await linkInside.isVisible()) { + await this.safeClick(`${carRowSelector} a, ${carRowSelector} button`, { timeout: IRACING_TIMEOUTS.elementWait }); + this.log('info', 'Clicked link/button inside car row fallback'); + return; + } + } + } + + // If none found, throw error + throw new Error('No Select button found in modal table and no fallback car row found'); } // NOTE: clickCarModalConfirm() and clickTrackModalConfirm() have been removed.