from playwright.async_api import Page # --- Helper Function for Replacing Sensitive Data --- def replace_sensitive_data(text: str, sensitive_map: dict) -> str: """Replaces sensitive data placeholders in text.""" if not isinstance(text, str): return text for placeholder, value in sensitive_map.items(): replacement_value = str(value) if value is not None else '' text = text.replace(f'{placeholder}', replacement_value) return text # --- Helper Function for Robust Action Execution --- class PlaywrightActionError(Exception): """Custom exception for errors during Playwright script action execution.""" pass async def _try_locate_and_act(page: Page, selector: str, action_type: str, text: str | None = None, step_info: str = '') -> None: """ Attempts an action (click/fill) with XPath fallback by trimming prefixes. Raises PlaywrightActionError if the action fails after all fallbacks. """ print(f'Attempting {action_type} ({step_info}) using selector: {repr(selector)}') original_selector = selector MAX_FALLBACKS = 50 # Increased fallbacks # Increased timeouts for potentially slow pages INITIAL_TIMEOUT = 10000 # Milliseconds for the first attempt (10 seconds) FALLBACK_TIMEOUT = 1000 # Shorter timeout for fallback attempts (1 second) try: locator = page.locator(selector).first if action_type == 'click': await locator.click(timeout=INITIAL_TIMEOUT) elif action_type == 'fill' and text is not None: await locator.fill(text, timeout=INITIAL_TIMEOUT) else: # This case should ideally not happen if called correctly raise PlaywrightActionError(f"Invalid action_type '{action_type}' or missing text for fill. ({step_info})") print(f" Action '{action_type}' successful with original selector.") await page.wait_for_timeout(500) # Wait after successful action return # Successful exit except Exception as e: print(f" Warning: Action '{action_type}' failed with original selector ({repr(selector)}): {e}. Starting fallback...") # Fallback only works for XPath selectors if not selector.startswith('xpath='): # Raise error immediately if not XPath, as fallback won't work raise PlaywrightActionError( f"Action '{action_type}' failed. Fallback not possible for non-XPath selector: {repr(selector)}. ({step_info})" ) xpath_parts = selector.split('=', 1) if len(xpath_parts) < 2: raise PlaywrightActionError( f"Action '{action_type}' failed. Could not extract XPath string from selector: {repr(selector)}. ({step_info})" ) xpath = xpath_parts[1] # Correctly get the XPath string segments = [seg for seg in xpath.split('/') if seg] for i in range(1, min(MAX_FALLBACKS + 1, len(segments))): trimmed_xpath_raw = '/'.join(segments[i:]) fallback_xpath = f'xpath=//{trimmed_xpath_raw}' print(f' Fallback attempt {i}/{MAX_FALLBACKS}: Trying selector: {repr(fallback_xpath)}') try: locator = page.locator(fallback_xpath).first if action_type == 'click': await locator.click(timeout=FALLBACK_TIMEOUT) elif action_type == 'fill' and text is not None: try: await locator.clear(timeout=FALLBACK_TIMEOUT) await page.wait_for_timeout(100) except Exception as clear_error: print(f' Warning: Failed to clear field during fallback ({step_info}): {clear_error}') await locator.fill(text, timeout=FALLBACK_TIMEOUT) print(f" Action '{action_type}' successful with fallback selector: {repr(fallback_xpath)}") await page.wait_for_timeout(500) return # Successful exit after fallback except Exception as fallback_e: print(f' Fallback attempt {i} failed: {fallback_e}') if i == MAX_FALLBACKS: # Raise exception after exhausting fallbacks raise PlaywrightActionError( f"Action '{action_type}' failed after {MAX_FALLBACKS} fallback attempts. Original selector: {repr(original_selector)}. ({step_info})" ) # This part should not be reachable if logic is correct, but added as safeguard raise PlaywrightActionError(f"Action '{action_type}' failed unexpectedly for {repr(original_selector)}. ({step_info})")