fix(automation): ensure Cars panel nav-click + resilient selectors and retry for add-car flow

This commit is contained in:
2025-11-26 18:41:59 +01:00
parent cd810b024c
commit d08f9e5264

View File

@@ -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.