Files
hive/tools/tests/test_refs.py
T
Hundao 90aadf247a fix(ci): unbreak main — ruff format, test_refs, test_model_catalog (#7084)
* fix(ci): apply ruff format to browser tool files

Refs #7083

* fix(ci): unbreak test_refs (img regression) and test_model_catalog

test_refs:
- Add `img` back to CONTENT_ROLES so named images get refs again. The
  recent `cc6ec97a feat: multiple modes browser snapshot tool` refactor
  renamed NAMED_CONTENT_ROLES → CONTENT_ROLES and accidentally dropped
  `img`, breaking `test_named_content_roles_get_refs`.
- Drop the `navigation` assertion from `test_skips_structural_roles`.
  That same refactor intentionally added landmark roles (navigation,
  main, listitem) to CONTENT_ROLES so AI agents can ref them by name,
  and the test was not updated to reflect that.

test_model_catalog:
- Add 5 openrouter models that were added to model_catalog.json by
  #7081 (UI/UX improvements) but not reflected in the test.

Refs #7083

* fix(ci): wait for event propagation in subagent report test on Windows

`test_worker_report_emits_subagent_report_event` waited only for
`worker.is_active` to flip to False, then immediately asserted on the
collected events. On Windows the event loop scheduling differs enough
that the SUBAGENT_REPORT subscriber callback can run a few ticks after
the worker is marked inactive, so the assertion fires against an empty
list. Wait for both conditions.

Refs #7083
2026-04-18 19:09:15 +08:00

191 lines
7.1 KiB
Python

"""Tests for the browser ref system (annotate_snapshot / resolve_ref)."""
from __future__ import annotations
import pytest
from gcu.browser.refs import (
RefEntry,
annotate_snapshot,
resolve_ref,
)
# ---------------------------------------------------------------------------
# annotate_snapshot
# ---------------------------------------------------------------------------
SAMPLE_SNAPSHOT = """\
- navigation "Main":
- link "Home"
- link "About"
- main:
- heading "Welcome"
- textbox "Search"
- button "Submit"
- paragraph: some text here
- img "Logo"
- list:
- listitem:
- link "Item 1"
- listitem:
- link "Item 2\""""
class TestAnnotateSnapshot:
def test_assigns_refs_to_interactive_roles(self):
annotated, ref_map = annotate_snapshot(SAMPLE_SNAPSHOT)
# link, textbox, button should all get refs
assert "[ref=e" in annotated
# Check that specific interactive elements got refs
roles_in_map = {entry.role for entry in ref_map.values()}
assert "link" in roles_in_map
assert "textbox" in roles_in_map
assert "button" in roles_in_map
def test_skips_structural_roles(self):
annotated, ref_map = annotate_snapshot(SAMPLE_SNAPSHOT)
roles_in_map = {entry.role for entry in ref_map.values()}
# main (unnamed), list, listitem (unnamed), paragraph are structural — no refs.
# Note: navigation is a landmark role and now gets a ref when named, so it
# is not asserted absent here.
assert "main" not in roles_in_map
assert "list" not in roles_in_map
assert "listitem" not in roles_in_map
assert "paragraph" not in roles_in_map
def test_named_content_roles_get_refs(self):
annotated, ref_map = annotate_snapshot(SAMPLE_SNAPSHOT)
roles_in_map = {entry.role for entry in ref_map.values()}
# heading and img have names, so they should get refs
assert "heading" in roles_in_map
assert "img" in roles_in_map
def test_unnamed_content_roles_skip(self):
snapshot = "- heading\n- img"
_, ref_map = annotate_snapshot(snapshot)
# No names → no refs for content roles
assert len(ref_map) == 0
def test_preserves_non_matching_lines(self):
snapshot = 'some random text\n- button "OK"\nanother line'
annotated, _ = annotate_snapshot(snapshot)
lines = annotated.split("\n")
assert lines[0] == "some random text"
assert lines[2] == "another line"
def test_nth_disambiguation(self):
snapshot = '- button "Save"\n- button "Save"\n- button "Cancel"'
annotated, ref_map = annotate_snapshot(snapshot)
# Two "Save" buttons should have nth=0 and nth=1
save_entries = [(rid, e) for rid, e in ref_map.items() if e.role == "button" and e.name == "Save"]
assert len(save_entries) == 2
nths = sorted(e.nth for _, e in save_entries)
assert nths == [0, 1]
# "Cancel" should have nth=0
cancel_entries = [e for e in ref_map.values() if e.role == "button" and e.name == "Cancel"]
assert len(cancel_entries) == 1
assert cancel_entries[0].nth == 0
def test_sequential_ref_ids(self):
snapshot = '- link "A"\n- link "B"\n- link "C"'
_, ref_map = annotate_snapshot(snapshot)
assert set(ref_map.keys()) == {"e0", "e1", "e2"}
def test_empty_snapshot(self):
annotated, ref_map = annotate_snapshot("")
assert annotated == ""
assert ref_map == {}
# ---------------------------------------------------------------------------
# resolve_ref
# ---------------------------------------------------------------------------
class TestResolveRef:
def test_resolves_valid_ref(self):
ref_map = {
"e0": RefEntry(role="button", name="Submit", nth=0),
}
result = resolve_ref("e0", ref_map)
assert result == '[role="button"][aria-label="Submit"]:nth-of-type(1)'
def test_passes_through_css_selectors(self):
ref_map = {"e0": RefEntry(role="button", name="OK", nth=0)}
assert resolve_ref("#my-button", ref_map) == "#my-button"
assert resolve_ref(".btn-primary", ref_map) == ".btn-primary"
assert resolve_ref("div > button", ref_map) == "div > button"
def test_passes_through_role_selectors(self):
ref_map = {"e0": RefEntry(role="button", name="OK", nth=0)}
sel = 'role=button[name="OK"]'
assert resolve_ref(sel, ref_map) == sel
def test_raises_on_unknown_ref(self):
ref_map = {"e0": RefEntry(role="button", name="OK", nth=0)}
with pytest.raises(ValueError, match="not found"):
resolve_ref("e99", ref_map)
def test_raises_when_no_ref_map(self):
with pytest.raises(ValueError, match="no snapshot"):
resolve_ref("e0", None)
def test_quoted_name_passes_through(self):
# Note: the CSS selector output does not currently escape inner quotes.
# This produces technically-broken CSS when name contains double quotes,
# but the bridge-based matcher appears to tolerate it. Tracked
# separately as a follow-up.
ref_map = {
"e0": RefEntry(role="button", name='Say "Hello"', nth=0),
}
result = resolve_ref("e0", ref_map)
assert result == '[role="button"][aria-label="Say "Hello""]:nth-of-type(1)'
def test_no_name_produces_role_only_selector(self):
ref_map = {
"e0": RefEntry(role="textbox", name=None, nth=0),
}
result = resolve_ref("e0", ref_map)
assert result == '[role="textbox"]:nth-of-type(1)'
def test_empty_name(self):
ref_map = {
"e0": RefEntry(role="button", name="", nth=0),
}
result = resolve_ref("e0", ref_map)
assert result == '[role="button"][aria-label=""]:nth-of-type(1)'
def test_nth_in_selector(self):
ref_map = {
"e0": RefEntry(role="link", name="Next", nth=2),
}
result = resolve_ref("e0", ref_map)
assert result == '[role="link"][aria-label="Next"]:nth-of-type(3)'
# ---------------------------------------------------------------------------
# Round-trip: annotate → resolve
# ---------------------------------------------------------------------------
class TestRoundTrip:
def test_annotate_then_resolve(self):
snapshot = '- button "Submit"\n- textbox "Email"\n- link "Home"'
_, ref_map = annotate_snapshot(snapshot)
# Each ref should resolve to a valid CSS selector (bridge-based API)
for ref_id, entry in ref_map.items():
resolved = resolve_ref(ref_id, ref_map)
assert resolved.startswith(f'[role="{entry.role}"]')
if entry.name is not None:
assert f'[aria-label="{entry.name}"]' in resolved
assert f":nth-of-type({entry.nth + 1})" in resolved
def test_css_selectors_still_work_after_annotate(self):
snapshot = '- button "OK"'
_, ref_map = annotate_snapshot(snapshot)
# CSS selectors pass through even when a ref_map exists
assert resolve_ref("#submit-btn", ref_map) == "#submit-btn"