utils/flutter_web_drive/tests/test_cli.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

159 lines
5.0 KiB
Python

"""CLI: argument parsing, verb dispatch, exit codes and stderr messages."""
from __future__ import annotations
import io
from pathlib import Path
from typing import Any, ClassVar
import pytest
from flutter_web_drive import cli
from flutter_web_drive.errors import EXIT_BROWSER, EXIT_USAGE, DriveError
class Recorder:
"""Stands in for Driver: records each call and returns its name."""
instances: ClassVar[list[Recorder]] = []
def __init__(self, app: str, *, cdp_port: int | None = None) -> None:
self.app = app
self.cdp_port = cdp_port
self.calls: list[tuple[str, tuple[Any, ...], dict[str, Any]]] = []
Recorder.instances.append(self)
def __getattr__(self, name: str) -> Any:
def verb(*args: Any, **kw: Any) -> str:
self.calls.append((name, args, kw))
if name == "stop":
raise DriveError("stop", "boom", EXIT_BROWSER)
return f"ok {name}"
return verb
@pytest.fixture(autouse=True)
def _recorder(monkeypatch: pytest.MonkeyPatch) -> None:
Recorder.instances = []
monkeypatch.setattr(cli, "Driver", Recorder)
def _run(argv: list[str], stdin: str = "") -> tuple[int, Any]:
code = cli.main(argv, stdin=io.StringIO(stdin))
return code, Recorder.instances[-1].calls[-1] if Recorder.instances else None
@pytest.mark.parametrize(
("argv", "call"),
[
(
["start", "--url", "http://u/", "--profile", "/p", "--cdp-port", "9341"],
(
"start",
("http://u/", Path("/p"), 9341),
{"unit": None, "size": "1400x1000"},
),
),
(["shot"], ("shot", (None,), {"min_colours": 0})),
(
["shot", "o.png", "--min-colours", "2"],
("shot", (Path("o.png"),), {"min_colours": 2}),
),
(
["tree", "--filter", "x", "--all", "--row", "R"],
("tree", (), {"filt": "x", "show_all": True, "row": "R"}),
),
(["tap", "Browse", "--contains"], ("tap", ("Browse",), {"contains": True})),
(["tap-xy", "1", "2.5"], ("tap_xy", (1.0, 2.5), {})),
(["text", "hello"], ("text", ("hello",), {})),
(["key", "Enter"], ("key", ("Enter",), {})),
(["scroll", "0", "300"], ("scroll", (0.0, 300.0), {"at": None})),
(
["scroll", "0", "3", "--at", "4", "5"],
("scroll", (0.0, 3.0), {"at": (4.0, 5.0)}),
),
(
["wait", "Library", "--timeout", "3", "--gone", "--flag", "current"],
(
"wait",
("Library",),
{
"timeout": 3.0,
"contains": False,
"gone": True,
"flag": "current",
"row": None,
},
),
),
(["eval", "1+1"], ("eval", ("1+1",), {})),
(
["console", "--limit", "5", "--warnings", "--fail-on-error"],
("console", (), {"limit": 5, "warnings": True, "fail_on_error": True}),
),
],
)
def test_verbs_dispatch(
argv: list[str], call: Any, capsys: pytest.CaptureFixture[str]
) -> None:
code, got = _run(argv)
assert code == 0
assert got == call
assert capsys.readouterr().out.startswith("ok ")
def test_global_app_and_port() -> None:
cli.main(["--app", "kartka", "--cdp-port", "9", "tree"], stdin=io.StringIO())
assert (Recorder.instances[-1].app, Recorder.instances[-1].cdp_port) == (
"kartka",
9,
)
def test_app_defaults_from_env(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("FWD_APP", "envapp")
cli.main(["tree"], stdin=io.StringIO())
assert Recorder.instances[-1].app == "envapp"
def test_text_stdin_strips_one_trailing_newline() -> None:
_, call = _run(["text", "--stdin"], stdin="p@ss\n")
assert call == ("text", ("p@ss",), {})
def test_text_file_reads_secret_without_argv(tmp_path: Path) -> None:
secret = tmp_path / "pw"
secret.write_text("s3cret\n")
_, call = _run(["text", "--file", str(secret)])
assert call == ("text", ("s3cret",), {})
def test_text_file_missing_is_usage_error(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
assert (
cli.main(["text", "--file", str(tmp_path / "nope")], stdin=io.StringIO())
== EXIT_USAGE
)
assert "cannot read" in capsys.readouterr().err
def test_text_needs_value_or_stdin(capsys: pytest.CaptureFixture[str]) -> None:
assert cli.main(["text"], stdin=io.StringIO()) == EXIT_USAGE
assert "text needs a value, --stdin or --file" in capsys.readouterr().err
def test_drive_error_becomes_exit_code_and_one_line(
capsys: pytest.CaptureFixture[str],
) -> None:
code, _ = _run(["stop"])
assert code == EXIT_BROWSER
assert capsys.readouterr().err == "flutter_web_drive: stop failed: boom\n"
def test_usage_errors_exit_2() -> None:
with pytest.raises(SystemExit) as err:
cli.main(["bogus"], stdin=io.StringIO())
assert err.value.code == EXIT_USAGE