diff --git a/Dockers/puppeteer-api/app/routes/browser.py b/Dockers/puppeteer-api/app/routes/browser.py index ff30d42..c470687 100644 --- a/Dockers/puppeteer-api/app/routes/browser.py +++ b/Dockers/puppeteer-api/app/routes/browser.py @@ -19,6 +19,7 @@ router = APIRouter(tags=["Browser"]) class ClickInteraction(BaseModel): action: Literal["click"] selector: Optional[str] = None + xpath_selector: Optional[str] = None x: Optional[int] = None y: Optional[int] = None @@ -26,6 +27,7 @@ class ClickInteraction(BaseModel): class TypeInteraction(BaseModel): action: Literal["type"] selector: Optional[str] = None + xpath_selector: Optional[str] = None text: str x: Optional[int] = None y: Optional[int] = None diff --git a/Dockers/puppeteer-api/app/services/browser.py b/Dockers/puppeteer-api/app/services/browser.py index c025307..2440732 100644 --- a/Dockers/puppeteer-api/app/services/browser.py +++ b/Dockers/puppeteer-api/app/services/browser.py @@ -289,10 +289,12 @@ async def interactions_service( if isinstance(interaction, dict): action = interaction.get("action") selector = interaction.get("selector") + xpath_selector = interaction.get("xpath_selector") text = interaction.get("text") else: action = getattr(interaction, "action", None) selector = getattr(interaction, "selector", None) + xpath_selector = getattr(interaction, "xpath_selector", None) text = getattr(interaction, "text", None) # Coordinates may be present on interaction models. x = getattr(interaction, "x", None) @@ -305,38 +307,77 @@ async def interactions_service( if not action: raise ValueError("Interaction must contain an action") - # Prefer selector-based interactions when selector is provided. - if selector: + # Prefer selector-based interactions when selectors are provided (CSS first, then XPath). + if selector or xpath_selector: if action == "click": - await page.wait_for_selector(selector, timeout=30000) - # Avoid Playwright auto-scrolling: click via DOM. - await page.evaluate( - """(sel) => { - const el = document.querySelector(sel); - if (!el) throw new Error(`No element for selector: ${sel}`); - el.click(); - }""", - selector, - ) + if selector: + await page.wait_for_selector(selector, timeout=30000) + # Avoid Playwright auto-scrolling: click via DOM. + await page.evaluate( + """(sel) => { + const el = document.querySelector(sel); + if (!el) throw new Error(`No element for selector: ${sel}`); + el.click(); + }""", + selector, + ) + else: + await page.wait_for_selector(f"xpath={xpath_selector}", timeout=30000) + await page.evaluate( + """(xpath) => { + const el = document.evaluate( + xpath, + document, + null, + XPathResult.FIRST_ORDERED_NODE_TYPE, + null + ).singleNodeValue; + if (!el) throw new Error(`No element for xpath: ${xpath}`); + el.click(); + }""", + xpath_selector, + ) elif action == "type": if text is None: raise ValueError("Type interaction must contain text") - await page.wait_for_selector(selector, timeout=30000) - # Avoid Playwright auto-scrolling: focus/fill via DOM. - await page.evaluate( - """({ sel, value }) => { - const el = document.querySelector(sel); - if (!el) throw new Error(`No element for selector: ${sel}`); - el.focus?.(); - if ('value' in el) { - el.value = value; - el.dispatchEvent(new Event('input', { bubbles: true })); - el.dispatchEvent(new Event('change', { bubbles: true })); - } - }""", - {"sel": selector, "value": text}, - ) + if selector: + await page.wait_for_selector(selector, timeout=30000) + # Avoid Playwright auto-scrolling: focus/fill via DOM. + await page.evaluate( + """({ sel, value }) => { + const el = document.querySelector(sel); + if (!el) throw new Error(`No element for selector: ${sel}`); + el.focus?.(); + if ('value' in el) { + el.value = value; + el.dispatchEvent(new Event('input', { bubbles: true })); + el.dispatchEvent(new Event('change', { bubbles: true })); + } + }""", + {"sel": selector, "value": text}, + ) + else: + await page.wait_for_selector(f"xpath={xpath_selector}", timeout=30000) + await page.evaluate( + """({ xpath, value }) => { + const el = document.evaluate( + xpath, + document, + null, + XPathResult.FIRST_ORDERED_NODE_TYPE, + null + ).singleNodeValue; + if (!el) throw new Error(`No element for xpath: ${xpath}`); + el.focus?.(); + if ('value' in el) { + el.value = value; + el.dispatchEvent(new Event('input', { bubbles: true })); + el.dispatchEvent(new Event('change', { bubbles: true })); + } + }""", + {"xpath": xpath_selector, "value": text}, + ) else: raise ValueError(f"Unknown interaction action: {action}") @@ -383,7 +424,7 @@ async def interactions_service( raise ValueError(f"Unknown interaction action: {action}") else: - raise ValueError("Interaction must contain either selector or x/y coordinates") + raise ValueError("Interaction must contain selector, xpath_selector, or x/y coordinates") # Let the UI settle after each action. await page.wait_for_timeout(1000)