"""Windows UIA locator compiler (Name ≠ Value; no coordinate primary).""" from __future__ import annotations from typing import Any, Optional from models.locator import LocatorCandidate from .base import count_matches, rank_and_select_primary def _expr(strategy: str, props: dict[str, Any]) -> str: parts = [f"{k}={v!r}" for k, v in props.items() if v not in (None, "")] return f"{strategy}:" + ",".join(parts) def compile_windows_locators( element: dict[str, Any], *, rows: Optional[list[dict[str, Any]]] = None, window_scope: Optional[dict[str, Any]] = None, ) -> list[LocatorCandidate]: """ Priority: 1. Window scope + AutomationId + ControlType 2. Window scope + stable Name + ControlType 3. Window scope + Name + Value + ControlType 4. Business parent + child attrs 5. Visible Value + ControlType 6. Relative ancestor scope 7. Index with business reason (low score) 8. BoundingRectangle evidence only """ el = element or {} rows = list(rows or []) win = dict(window_scope or el.get("window_scope") or {}) scope = { "window_class": win.get("class_name") or win.get("window_class") or "", "window_title": win.get("title") or win.get("window_title") or "", "process_name": win.get("process_name") or "", } cands: list[LocatorCandidate] = [] aid = str(el.get("automation_id") or "").strip() name = str(el.get("name") or "").strip() value = str(el.get("value") or "").strip() ctype = str(el.get("control_type") or el.get("role") or "").strip() class_name = str(el.get("class_name") or "").strip() parent_chain = list(el.get("parent_chain") or []) def add( strategy: str, props: dict[str, Any], score: float, reason: str, *, uses_index: bool = False, uses_coordinates: bool = False, ) -> None: if not props or not any(str(v).strip() for v in props.values() if not isinstance(v, (dict, list))): return c = LocatorCandidate( strategy=strategy, scope=dict(scope), properties=dict(props), expression=_expr(strategy, props), stability_score=score, uses_index=uses_index, uses_coordinates=uses_coordinates, reasons=[reason], ) if rows: c.mark_counts(count_matches(rows, properties=props, scope=scope)) c.validation = "snapshot_only" elif uses_coordinates: c.production_eligible = False c.unique = False c.ambiguous = True cands.append(c) if aid and ctype: add( "window_automation_id_type", {"automation_id": aid, "control_type": ctype}, 0.96, "automation_id_control_type", ) elif aid: add("window_automation_id", {"automation_id": aid}, 0.9, "automation_id") if name and ctype and name.lower() not in {"item", "pane", "unknown"}: add( "window_name_type", {"name": name, "control_type": ctype}, 0.88, "stable_name_control_type", ) if name and value and ctype: add( "window_name_value_type", {"name": name, "value": value, "control_type": ctype}, 0.86, "name_value_control_type", ) # Business parent + child if parent_chain: parent = parent_chain[0] if isinstance(parent_chain[0], dict) else {} parent_name = str(parent.get("name") or parent.get("automation_id") or "").strip() if parent_name and (aid or name or value): props: dict[str, Any] = {"parent_name": parent_name} if aid: props["automation_id"] = aid if name: props["name"] = name if value: props["value"] = value if ctype: props["control_type"] = ctype add("parent_scoped", props, 0.82, "business_parent_scope") if value and ctype and len(value) <= 64: add( "value_type", {"value": value, "control_type": ctype}, 0.78, "visible_value_control_type", ) if class_name and ctype: add( "class_type", {"class_name": class_name, "control_type": ctype}, 0.55, "class_name_control_type", ) # Index only with explicit business reason — low score, not auto primary idx = el.get("index") if idx is not None and (aid or name): props = {"index": int(idx)} if aid: props["automation_id"] = aid if name: props["name"] = name if ctype: props["control_type"] = ctype add( "indexed", props, 0.35, "index_requires_business_justification", uses_index=True, ) bounds = el.get("bounds") or el.get("rect") or el.get("bounding_rectangle") if bounds: c = LocatorCandidate( strategy="bounding_rectangle", scope=dict(scope), properties={"bounds": bounds}, expression="coordinates:(evidence_only)", stability_score=0.05, uses_coordinates=True, production_eligible=False, unique=False, ambiguous=True, reasons=["coordinates_never_primary"], ) cands.append(c) for c in cands: if c.actual_count is not None: c.mark_counts(int(c.actual_count), expected=int(c.expected_count or 1)) if c.uses_coordinates: c.production_eligible = False rank_and_select_primary(cands) return cands