wip
This commit is contained in:
@@ -2,6 +2,7 @@ import { Result } from '../../../shared/result/Result';
|
||||
import { CheckoutPrice } from '../../../domain/value-objects/CheckoutPrice';
|
||||
import { CheckoutState } from '../../../domain/value-objects/CheckoutState';
|
||||
import { CheckoutInfo } from '../../../application/ports/ICheckoutService';
|
||||
import { IRACING_SELECTORS } from './IRacingSelectors';
|
||||
|
||||
interface Page {
|
||||
locator(selector: string): Locator;
|
||||
@@ -14,14 +15,15 @@ interface Locator {
|
||||
}
|
||||
|
||||
export class CheckoutPriceExtractor {
|
||||
private readonly selector = '.wizard-footer a.btn:has(span.label-pill)';
|
||||
// Use the price action selector from IRACING_SELECTORS
|
||||
private readonly selector = IRACING_SELECTORS.BLOCKED_SELECTORS.priceAction;
|
||||
|
||||
constructor(private readonly page: Page) {}
|
||||
|
||||
async extractCheckoutInfo(): Promise<Result<CheckoutInfo>> {
|
||||
try {
|
||||
// Prefer the explicit pill element which contains the price
|
||||
const pillLocator = this.page.locator('span.label-pill');
|
||||
const pillLocator = this.page.locator('.label-pill, .label-inverse');
|
||||
const pillText = await pillLocator.first().textContent().catch(() => null);
|
||||
|
||||
let price: CheckoutPrice | null = null;
|
||||
@@ -68,7 +70,7 @@ export class CheckoutPriceExtractor {
|
||||
// Additional fallback: search the wizard-footer for any price text if pill was not present or parsing failed
|
||||
if (!price) {
|
||||
try {
|
||||
const footerLocator = this.page.locator('.wizard-footer').first();
|
||||
const footerLocator = this.page.locator('.wizard-footer, .modal-footer').first();
|
||||
const footerText = await footerLocator.textContent().catch(() => null);
|
||||
if (footerText) {
|
||||
const match = footerText.match(/\$\d+\.\d{2}/);
|
||||
|
||||
@@ -87,7 +87,7 @@ export const IRACING_SELECTORS = {
|
||||
// Form groups have labels followed by inputs
|
||||
sessionName: '#set-session-information .card-block .form-group:first-of-type input.form-control',
|
||||
sessionNameAlt: '#set-session-information input.form-control[type="text"]:not([maxlength])',
|
||||
password: '#set-session-information .card-block .form-group:nth-of-type(2) input.form-control',
|
||||
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:"])',
|
||||
passwordAlt: '#set-session-information input.form-control[maxlength="32"]',
|
||||
description: '#set-session-information .card-block .form-group:last-of-type textarea.form-control',
|
||||
descriptionAlt: '#set-session-information textarea.form-control',
|
||||
|
||||
@@ -995,9 +995,10 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
if (this.isRealMode()) {
|
||||
await this.clickNewRaceInModal();
|
||||
// Ensure Race Information panel is visible by clicking sidebar nav then waiting for fallback selectors
|
||||
const raceInfoFallback = '#set-session-information, .wizard-step[id*="session"], .wizard-step[id*="race-information"]';
|
||||
const raceInfoFallback = IRACING_SELECTORS.wizard.stepContainers.raceInformation;
|
||||
const raceInfoNav = IRACING_SELECTORS.wizard.sidebarLinks.raceInformation;
|
||||
try {
|
||||
try { await this.page!.click('[data-testid="wizard-nav-set-session-information"]'); this.log('debug','Clicked wizard nav for Race Information', { selector: '[data-testid="wizard-nav-set-session-information"]' }); } catch (e) { this.log('debug','Wizard nav for Race Information not present (continuing)', { error: String(e) }); }
|
||||
try { await this.page!.click(raceInfoNav); this.log('debug','Clicked wizard nav for Race Information', { selector: raceInfoNav }); } catch (e) { this.log('debug','Wizard nav for Race Information not present (continuing)', { error: String(e) }); }
|
||||
await this.page!.waitForSelector(raceInfoFallback, { state: 'attached', timeout: 5000 });
|
||||
this.log('info','Race Information panel found', { selector: raceInfoFallback });
|
||||
} catch (err) {
|
||||
@@ -1005,7 +1006,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
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-session-information"]'); } catch {}
|
||||
try { await this.page!.click(raceInfoNav); } catch {}
|
||||
await this.page!.waitForSelector(raceInfoFallback, { state: 'attached', timeout: 10000 });
|
||||
}
|
||||
}
|
||||
@@ -1096,12 +1097,13 @@ 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, #select-car-compact-content, .cars-panel, [id*="select-car"], [data-step="set-cars"]';
|
||||
const carsFallbackSelector = IRACING_SELECTORS.wizard.stepContainers.cars;
|
||||
const carsNav = IRACING_SELECTORS.wizard.sidebarLinks.cars;
|
||||
try {
|
||||
this.log('debug', 'nav-click attempted for Cars', { navSelector: '[data-testid="wizard-nav-set-cars"]' });
|
||||
this.log('debug', 'nav-click attempted for Cars', { navSelector: carsNav });
|
||||
// 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"]' });
|
||||
await this.page!.click(carsNav).catch(() => {});
|
||||
this.log('debug', 'Primary nav-click attempted', { selector: carsNav });
|
||||
|
||||
try {
|
||||
this.log('debug', 'Waiting for Cars panel using primary selector', { selector: carsFallbackSelector });
|
||||
@@ -1113,7 +1115,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
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!.click(carsNav).catch(() => {});
|
||||
await this.page!.waitForSelector(carsFallbackSelector, { state: 'attached', timeout: 10000 });
|
||||
this.log('info', 'Cars panel found after retry', { selector: carsFallbackSelector });
|
||||
}
|
||||
@@ -1184,7 +1186,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
|
||||
// Check if we're on Track page (Step 11) instead of Cars page
|
||||
const onTrackPage = wizardFooter.includes('Track Options') ||
|
||||
await this.page!.locator('#set-track').isVisible().catch(() => false);
|
||||
await this.page!.locator(IRACING_SELECTORS.wizard.stepContainers.track).isVisible().catch(() => false);
|
||||
|
||||
if (onTrackPage) {
|
||||
const errorMsg = `FATAL: Step 9 attempted on Track page (Step 11) - navigation bug detected. Wizard footer: "${wizardFooter}"`;
|
||||
@@ -1278,7 +1280,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
this.log('info', 'Step 11: Validating page state before proceeding');
|
||||
const step11Validation = await this.validatePageState({
|
||||
expectedStep: 'track',
|
||||
requiredSelectors: ['#set-track'], // Both modes use same container ID
|
||||
requiredSelectors: [IRACING_SELECTORS.wizard.stepContainers.track], // Both modes use same container ID
|
||||
forbiddenSelectors: this.isRealMode()
|
||||
? [IRACING_SELECTORS.steps.addCarButton]
|
||||
: [] // Mock mode: no forbidden selectors needed
|
||||
@@ -1430,11 +1432,12 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
}
|
||||
|
||||
// Robust: try opening Weather via sidebar nav then wait for a set of fallback selectors.
|
||||
const weatherFallbackSelector = '#set-weather, .wizard-step[id*="weather"], .wizard-step[data-step="weather"], .weather-panel';
|
||||
const weatherFallbackSelector = IRACING_SELECTORS.wizard.stepContainers.weather;
|
||||
const weatherNav = IRACING_SELECTORS.wizard.sidebarLinks.weather;
|
||||
try {
|
||||
try {
|
||||
await this.page!.click('[data-testid="wizard-nav-set-weather"]');
|
||||
this.log('debug', 'Clicked wizard nav for Weather', { selector: '[data-testid="wizard-nav-set-weather"]' });
|
||||
await this.page!.click(weatherNav);
|
||||
this.log('debug', 'Clicked wizard nav for Weather', { selector: weatherNav });
|
||||
} catch (e) {
|
||||
this.log('debug', 'Wizard nav for Weather not present (continuing)', { error: String(e) });
|
||||
}
|
||||
@@ -1447,7 +1450,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
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-weather"]'); } catch {}
|
||||
try { await this.page!.click(weatherNav); } catch {}
|
||||
await this.page!.waitForSelector(weatherFallbackSelector, { state: 'attached', timeout: 10000 });
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -1882,7 +1885,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
|
||||
try {
|
||||
// Check for Chakra UI modals (do NOT use this for datetime pickers - see dismissDatetimePickers)
|
||||
const modalContainer = this.page.locator('.chakra-modal__content-container');
|
||||
const modalContainer = this.page.locator('.chakra-modal__content-container, .modal-content');
|
||||
const isModalVisible = await modalContainer.isVisible().catch(() => false);
|
||||
|
||||
if (!isModalVisible) {
|
||||
@@ -1972,10 +1975,10 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
// Strategy 2: Click on the modal body outside the picker
|
||||
// This simulates clicking elsewhere to close the dropdown
|
||||
this.log('debug', `${stillOpenCount} picker(s) still open, clicking outside`);
|
||||
const modalBody = this.page.locator('.modal-body').first();
|
||||
const modalBody = this.page.locator(IRACING_SELECTORS.wizard.modalContent).first();
|
||||
if (await modalBody.isVisible().catch(() => false)) {
|
||||
// Click at a safe spot - the header area of the card
|
||||
const cardHeader = this.page.locator('#set-time-of-day .card-header').first();
|
||||
const cardHeader = this.page.locator(`${IRACING_SELECTORS.wizard.stepContainers.timeOfDay} .card-header`).first();
|
||||
if (await cardHeader.isVisible().catch(() => false)) {
|
||||
await cardHeader.click({ force: true, timeout: 1000 }).catch(() => {});
|
||||
await this.page.waitForTimeout(100);
|
||||
@@ -2411,7 +2414,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
try {
|
||||
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';
|
||||
const modalSelector = IRACING_SELECTORS.steps.addCarModal;
|
||||
await this.page.waitForSelector(modalSelector, {
|
||||
state: 'attached',
|
||||
timeout: this.isRealMode() ? IRACING_TIMEOUTS.elementWait : this.config.timeout,
|
||||
@@ -2426,7 +2429,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
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';
|
||||
const modalSelectorRetry = IRACING_SELECTORS.steps.addCarModal;
|
||||
await this.page.waitForSelector(modalSelectorRetry, {
|
||||
state: 'attached',
|
||||
timeout: 10000,
|
||||
@@ -2509,18 +2512,24 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
throw new Error('Browser not connected');
|
||||
}
|
||||
|
||||
// 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', { selector: directSelector });
|
||||
return;
|
||||
// First try direct select button (non-dropdown) - using verified selectors
|
||||
// Try both track and car select buttons as this method is shared
|
||||
const directSelectors = [
|
||||
IRACING_SELECTORS.steps.trackSelectButton,
|
||||
IRACING_SELECTORS.steps.carSelectButton
|
||||
];
|
||||
|
||||
for (const selector of directSelectors) {
|
||||
const button = this.page.locator(selector).first();
|
||||
if (await button.count() > 0 && await button.isVisible()) {
|
||||
await this.safeClick(selector, { timeout: IRACING_TIMEOUTS.elementWait });
|
||||
this.log('info', 'Clicked direct Select button for first search result', { selector });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: dropdown toggle pattern
|
||||
const dropdownSelector = '.modal table a.btn.btn-primary.btn-xs.dropdown-toggle';
|
||||
// Fallback: dropdown toggle pattern (for multi-config tracks)
|
||||
const dropdownSelector = IRACING_SELECTORS.steps.trackSelectDropdown;
|
||||
const dropdownButton = this.page.locator(dropdownSelector).first();
|
||||
|
||||
if (await dropdownButton.count() > 0 && await dropdownButton.isVisible()) {
|
||||
@@ -2532,7 +2541,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
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';
|
||||
const itemSelector = IRACING_SELECTORS.steps.trackSelectDropdownItem;
|
||||
await this.page.waitForTimeout(200);
|
||||
await this.safeClick(itemSelector, { timeout: IRACING_TIMEOUTS.elementWait });
|
||||
this.log('info', 'Clicked first dropdown item to select track config', { selector: itemSelector });
|
||||
@@ -2707,8 +2716,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
// Check for authenticated UI indicators
|
||||
// Look for elements that are ONLY present when authenticated
|
||||
const authSelectors = [
|
||||
'button:has-text("Create a Race")',
|
||||
'[aria-label="Create a Race"]',
|
||||
IRACING_SELECTORS.hostedRacing.createRaceButton,
|
||||
// User menu/profile indicators (present on ALL authenticated pages)
|
||||
'[aria-label*="user menu" i]',
|
||||
'[aria-label*="account menu" i]',
|
||||
@@ -3897,6 +3905,8 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
// Check for close button click or ESC key
|
||||
if (await this.isCloseRequested()) {
|
||||
this.log('info', 'Browser close requested by user (close button or ESC key)');
|
||||
// Only close if we are not in the middle of a critical operation or if explicitly confirmed
|
||||
// For now, we'll just log and throw, but we might want to add a confirmation dialog in the future
|
||||
await this.closeBrowserContext();
|
||||
throw new Error('USER_CLOSE_REQUESTED: Browser closed by user request');
|
||||
}
|
||||
@@ -4112,12 +4122,16 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
}
|
||||
|
||||
// ESC key listener - close browser on ESC press
|
||||
// DISABLED: ESC key is often used to close modals/popups in iRacing
|
||||
// We should only close on explicit close button click
|
||||
/*
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
console.log('[GridPilot] ESC key pressed, requesting close');
|
||||
(window as unknown as { __gridpilot_close_requested?: boolean }).__gridpilot_close_requested = true;
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
// Modal visibility observer - detect when wizard modal is closed
|
||||
// Look for Bootstrap modal backdrop disappearing or modal being hidden
|
||||
@@ -4129,14 +4143,18 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
||||
// Modal backdrop removed
|
||||
if (node.classList.contains('modal-backdrop')) {
|
||||
console.log('[GridPilot] Modal backdrop removed, checking if wizard dismissed');
|
||||
// Small delay to allow for legitimate modal transitions
|
||||
// Increased delay to allow for legitimate modal transitions (e.g. step changes)
|
||||
setTimeout(() => {
|
||||
// Check if ANY wizard-related modal is visible
|
||||
const wizardModal = document.querySelector('.modal.fade.in, .modal.show');
|
||||
if (!wizardModal) {
|
||||
// Also check if we are just transitioning between steps (sometimes modal is briefly hidden)
|
||||
const wizardContent = document.querySelector('.wizard-content, .wizard-step');
|
||||
|
||||
if (!wizardModal && !wizardContent) {
|
||||
console.log('[GridPilot] Wizard modal no longer visible, requesting close');
|
||||
(window as unknown as { __gridpilot_close_requested?: boolean }).__gridpilot_close_requested = true;
|
||||
}
|
||||
}, 500);
|
||||
}, 2000); // Increased from 500ms to 2000ms
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user