90aadf247a
* 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
191 lines
7.1 KiB
Python
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"
|