Keep the last in-template copy on main so history can restore it; product continues in jiangchang-ui-recorder. Co-authored-by: Cursor <cursoragent@cursor.com>
181 lines
5.7 KiB
Python
181 lines
5.7 KiB
Python
"""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
|