Newer
Older
cortex-hub / browser-service / src / core / resolver.py
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)