utils/flutter_web_drive/tests/test_semantics.py
Krzysztof kuhy Rudnicki f93a24e283 flutter_web_drive: headless CDP driver + MCP server for Flutter web apps
Drives a Flutter web build in a persistent headless chromium (systemd-run
unit, own profile, CDP port) through the semantics tree: tree, tap by label,
text, key, scroll, wait, shot, console. Never touches the live display;
attaches only to HeadlessChrome. The MCP server wraps the same Driver.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZ4sULGfyav7EEwQ6vhbfg
2026-10-24 22:37:49 +02:00

106 lines
4.0 KiB
Python

"""Semantics records: parsing, label matching, ranking and the compact tree."""
from __future__ import annotations
import pytest
from flutter_web_drive import semantics
from flutter_web_drive.errors import DriveError
from flutter_web_drive.tests.fakes import node
TAB = node(4, "Library\nTab 1 of 4", rect=(0, 8, 80, 67), flags=("current",))
HEADING = node(12, "Library", role="", tag="h2", tappable=False, rect=(16, 13, 72, 30))
CONTAINER = node(1, "", role="", tappable=False, rect=(0, 0, 1400, 900))
FIELD = node(
20, "Email", role="textbox", value="me@x", tappable=False, flags=("focused",)
)
def test_parse_nodes_builds_typed_records() -> None:
(parsed,) = semantics.parse_nodes([TAB])
assert parsed.id == 4
assert parsed.lines == ("Library", "Tab 1 of 4")
assert parsed.center == (40.0, 41.5)
assert parsed.flags == ("current",)
def test_parse_nodes_rejects_non_list() -> None:
with pytest.raises(DriveError, match="semantics"):
semantics.parse_nodes("nope")
def test_exact_match_hits_whole_label_or_any_line() -> None:
tab = semantics.parse_nodes([TAB])[0]
assert semantics.matches(tab, "Library")
assert semantics.matches(tab, "Library\nTab 1 of 4")
assert semantics.matches(tab, "Library | Tab 1 of 4") # as `tree` prints it
assert not semantics.matches(tab, "Lib")
assert semantics.matches(tab, "lib", contains=True)
assert not semantics.matches(tab, "Browse", contains=True)
def test_find_prefers_tappable_and_keeps_document_order() -> None:
nodes = semantics.parse_nodes(
[HEADING, TAB, node(9, "Library", rect=(0, 300, 10, 10))]
)
hits = semantics.find(nodes, "Library")
assert [h.id for h in hits] == [4, 9, 12]
def test_describe_is_one_line_with_tags_and_value() -> None:
tab, field = semantics.parse_nodes([TAB, FIELD])
assert (
semantics.describe(tab)
== '#4 button "Library | Tab 1 of 4" [tap,current] @0,8 80x67'
)
assert (
semantics.describe(field) == '#20 textbox "Email" = "me@x" [focused] @0,0 80x40'
)
heading = semantics.parse_nodes([HEADING])[0]
assert semantics.describe(heading) == '#12 h2 "Library" @16,13 72x30'
def test_format_tree_hides_empty_containers_unless_all() -> None:
nodes = semantics.parse_nodes([CONTAINER, TAB, FIELD])
compact = semantics.format_tree(nodes)
assert "#1 " not in compact
assert "#4 " in compact
assert "#1 " in semantics.format_tree(nodes, show_all=True)
def test_format_tree_filter_matches_label_role_or_value() -> None:
nodes = semantics.parse_nodes([TAB, FIELD])
assert semantics.format_tree(nodes, filt="textbox").count("\n") == 0
assert "#20" in semantics.format_tree(nodes, filt="ME@X")
assert semantics.format_tree(nodes, filt="zzz") == "(no matching semantics nodes)"
assert semantics.format_tree([]) == "(no semantics nodes)"
def test_hint_lists_tappable_labels_capped() -> None:
many = [node(i, f"b{i}") for i in range(15)] + [HEADING, node(99, "")]
hint = semantics.hint(semantics.parse_nodes(many))
assert hint.startswith('"b0", "b1"')
assert "b12" not in hint
assert hint.endswith("...")
assert semantics.hint(semantics.parse_nodes([HEADING])) == "(no tappable nodes)"
assert semantics.hint(semantics.parse_nodes([TAB])) == '"Library | Tab 1 of 4"'
def test_js_snippets_never_read_password_values() -> None:
assert "password" in semantics.COLLECT_JS
assert semantics.IMAGE_STATS_FN.startswith("(async")
def test_in_row_keeps_nodes_vertically_inside_the_anchor_row() -> None:
rows = semantics.parse_nodes(
[
node(1, "Chapter 2\n2024-01-02", rect=(0, 100, 1000, 60)),
node(2, "Mark unread", rect=(900, 110, 40, 40)),
node(3, "Chapter 1\n2024-01-01", rect=(0, 160, 1000, 60)),
node(4, "Mark read", rect=(900, 170, 40, 40)),
]
)
assert [n.id for n in semantics.in_row(rows, "Chapter 2")] == [2]
assert [n.id for n in semantics.in_row(rows, "chapter 1", contains=True)] == [4]
assert semantics.in_row(rows, "Chapter 9") == []