allow xpath selectors
Build and Push Docker Images / build-and-push (push) Successful in 1m13s

This commit is contained in:
2026-04-29 16:16:14 +02:00
parent a93bca3c73
commit be4719fe01
2 changed files with 71 additions and 28 deletions
@@ -19,6 +19,7 @@ router = APIRouter(tags=["Browser"])
class ClickInteraction(BaseModel): class ClickInteraction(BaseModel):
action: Literal["click"] action: Literal["click"]
selector: Optional[str] = None selector: Optional[str] = None
xpath_selector: Optional[str] = None
x: Optional[int] = None x: Optional[int] = None
y: Optional[int] = None y: Optional[int] = None
@@ -26,6 +27,7 @@ class ClickInteraction(BaseModel):
class TypeInteraction(BaseModel): class TypeInteraction(BaseModel):
action: Literal["type"] action: Literal["type"]
selector: Optional[str] = None selector: Optional[str] = None
xpath_selector: Optional[str] = None
text: str text: str
x: Optional[int] = None x: Optional[int] = None
y: Optional[int] = None y: Optional[int] = None
+69 -28
View File
@@ -289,10 +289,12 @@ async def interactions_service(
if isinstance(interaction, dict): if isinstance(interaction, dict):
action = interaction.get("action") action = interaction.get("action")
selector = interaction.get("selector") selector = interaction.get("selector")
xpath_selector = interaction.get("xpath_selector")
text = interaction.get("text") text = interaction.get("text")
else: else:
action = getattr(interaction, "action", None) action = getattr(interaction, "action", None)
selector = getattr(interaction, "selector", None) selector = getattr(interaction, "selector", None)
xpath_selector = getattr(interaction, "xpath_selector", None)
text = getattr(interaction, "text", None) text = getattr(interaction, "text", None)
# Coordinates may be present on interaction models. # Coordinates may be present on interaction models.
x = getattr(interaction, "x", None) x = getattr(interaction, "x", None)
@@ -305,38 +307,77 @@ async def interactions_service(
if not action: if not action:
raise ValueError("Interaction must contain an action") raise ValueError("Interaction must contain an action")
# Prefer selector-based interactions when selector is provided. # Prefer selector-based interactions when selectors are provided (CSS first, then XPath).
if selector: if selector or xpath_selector:
if action == "click": if action == "click":
await page.wait_for_selector(selector, timeout=30000) if selector:
# Avoid Playwright auto-scrolling: click via DOM. await page.wait_for_selector(selector, timeout=30000)
await page.evaluate( # Avoid Playwright auto-scrolling: click via DOM.
"""(sel) => { await page.evaluate(
const el = document.querySelector(sel); """(sel) => {
if (!el) throw new Error(`No element for selector: ${sel}`); const el = document.querySelector(sel);
el.click(); if (!el) throw new Error(`No element for selector: ${sel}`);
}""", el.click();
selector, }""",
) 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": elif action == "type":
if text is None: if text is None:
raise ValueError("Type interaction must contain text") raise ValueError("Type interaction must contain text")
await page.wait_for_selector(selector, timeout=30000) if selector:
# Avoid Playwright auto-scrolling: focus/fill via DOM. await page.wait_for_selector(selector, timeout=30000)
await page.evaluate( # Avoid Playwright auto-scrolling: focus/fill via DOM.
"""({ sel, value }) => { await page.evaluate(
const el = document.querySelector(sel); """({ sel, value }) => {
if (!el) throw new Error(`No element for selector: ${sel}`); const el = document.querySelector(sel);
el.focus?.(); if (!el) throw new Error(`No element for selector: ${sel}`);
if ('value' in el) { el.focus?.();
el.value = value; if ('value' in el) {
el.dispatchEvent(new Event('input', { bubbles: true })); el.value = value;
el.dispatchEvent(new Event('change', { bubbles: true })); el.dispatchEvent(new Event('input', { bubbles: true }));
} el.dispatchEvent(new Event('change', { bubbles: true }));
}""", }
{"sel": selector, "value": text}, }""",
) {"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: else:
raise ValueError(f"Unknown interaction action: {action}") raise ValueError(f"Unknown interaction action: {action}")
@@ -383,7 +424,7 @@ async def interactions_service(
raise ValueError(f"Unknown interaction action: {action}") raise ValueError(f"Unknown interaction action: {action}")
else: 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. # Let the UI settle after each action.
await page.wait_for_timeout(1000) await page.wait_for_timeout(1000)