import logging
logger = logging.getLogger(__name__)
class SelectorResolver:
def __init__(self, page, session_id, a11y_maps):
self.page = page
self.session_id = session_id
self.a11y_maps = a11y_maps
async def resolve(self, selector):
"""Resolves eX references back to Playwright locators."""
if selector and selector.startswith("e") and selector[1:].isdigit():
ref_map = self.a11y_maps.get(self.session_id, {})
node = ref_map.get(selector)
if not node:
raise ValueError(f"Reference '{selector}' not found in current snapshot. Run 'GetSnapshot' to refresh refs.")
role = node.get("role")
name = node.get("name")
tagName = node.get("tagName")
nth = node.get("nth", 0)
# Determine context (Frame or Page)
frame_url = node.get("frame_url")
context = self.page
if frame_url:
# Find the correct frame by URL
for frame in self.page.frames:
if frame.url == frame_url:
context = frame
break
logger.info(f"Resolving {selector}: role={role}, name={name}, nth={nth} (Frame: {frame_url or 'Main'})")
# Primary strategy: Role + Name + Nth
locator = context.get_by_role(role, name=name).nth(nth)
if await locator.count() > 0:
return locator
# Fallback 1: Text
if role in ["button", "link"] and name:
locator = context.get_by_text(name).nth(nth)
if await locator.count() > 0:
return locator
# Fallback 2: Placeholder
if node.get("placeholder"):
locator = context.get_by_placeholder(node.get("placeholder")).nth(nth)
if await locator.count() > 0:
return locator
# Fallback 3: Tag + Text
if tagName:
locator = context.locator(tagName.lower()).filter(has_text=name).nth(nth)
if await locator.count() > 0:
return locator
# Final Fallback: Coordinate click (Spatial failover) indicator
if "rect" in node:
logger.info(f"Falling back to spatial coordinates for {selector}")
# Note: mouse is still on the page object, coordinates are usually valid for the viewport
return self.page.mouse
raise ValueError(f"Locator {selector} exists in mapping but is no longer present on the page.")
return self.page.locator(selector)