188 lines
6.7 KiB
Python
188 lines
6.7 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()}
|
|
# navigation, main, list, listitem, paragraph are structural — no refs
|
|
assert "navigation" not in roles_in_map
|
|
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[name="Submit"] >> nth=0'
|
|
|
|
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_escapes_quotes_in_name(self):
|
|
ref_map = {
|
|
"e0": RefEntry(role="button", name='Say "Hello"', nth=0),
|
|
}
|
|
result = resolve_ref("e0", ref_map)
|
|
assert result == 'role=button[name="Say \\"Hello\\""] >> nth=0'
|
|
|
|
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=0"
|
|
|
|
def test_empty_name(self):
|
|
ref_map = {
|
|
"e0": RefEntry(role="button", name="", nth=0),
|
|
}
|
|
result = resolve_ref("e0", ref_map)
|
|
assert result == 'role=button[name=""] >> nth=0'
|
|
|
|
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[name="Next"] >> nth=2'
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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 Playwright role selector
|
|
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'name="{entry.name}"' in resolved
|
|
assert f"nth={entry.nth}" 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"
|