mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 21:07:15 +01:00
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
135 lines
4.3 KiB
Python
135 lines
4.3 KiB
Python
"""Verbs that report from one attached page: shots, eval, console, rows."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from flutter_web_drive import actions, cdp
|
|
from flutter_web_drive.errors import DriveError
|
|
from flutter_web_drive.tests.fakes import FakeClock, FakePage, node
|
|
|
|
TABS = [
|
|
node(4, "Library\nTab 1 of 4", rect=(0, 8, 80, 67), flags=("current",)),
|
|
node(5, "Browse\nTab 2 of 4", rect=(0, 75, 80, 67)),
|
|
node(12, "Library", role="", tag="h2", tappable=False),
|
|
]
|
|
|
|
|
|
def _sess(page: FakePage) -> tuple[cdp.Session, FakeClock]:
|
|
return cdp.Session(page.ws()), FakeClock()
|
|
|
|
|
|
def test_wait_for_timeout_names_screenshot() -> None:
|
|
sess, clock = _sess(FakePage(nodes=TABS))
|
|
with pytest.raises(DriveError, match=r"present after 1s; screenshot /s\.png"):
|
|
actions.wait_for(
|
|
sess, clock, "Library", timeout=1, gone=True, on_timeout=lambda: "/s.png"
|
|
)
|
|
with pytest.raises(DriveError, match="not present"):
|
|
actions.wait_for(sess, clock, "Nope", timeout=1, on_timeout=lambda: "/s.png")
|
|
|
|
|
|
def test_screenshot_writes_png_and_reports_stats(tmp_path: Path) -> None:
|
|
sess = cdp.Session(FakePage().ws())
|
|
out = tmp_path / "a" / "s.png"
|
|
msg = actions.screenshot(sess, out, min_colours=2)
|
|
assert out.read_bytes().startswith(b"\x89PNG")
|
|
assert msg == f"{out} 1400x900 colours=180 stddev=9.0"
|
|
|
|
|
|
def test_screenshot_flags_blank_frames(tmp_path: Path) -> None:
|
|
page = FakePage(stats={"w": 10, "h": 10, "colours": 1, "stddev": 0.0})
|
|
with pytest.raises(DriveError, match="looks blank"):
|
|
actions.screenshot(cdp.Session(page.ws()), tmp_path / "b.png", min_colours=2)
|
|
|
|
|
|
def test_evaluate_serialises_value() -> None:
|
|
sess = cdp.Session(FakePage(evals={"x": {"a": [1]}}).ws())
|
|
assert actions.evaluate(sess, "x") == '{"a": [1]}'
|
|
|
|
|
|
EVENTS = [
|
|
{
|
|
"method": "Runtime.consoleAPICalled",
|
|
"params": {"type": "log", "args": [{"value": "hi"}]},
|
|
},
|
|
{
|
|
"method": "Runtime.consoleAPICalled",
|
|
"params": {"type": "error", "args": [{"value": "bad"}, {"description": "Obj"}]},
|
|
},
|
|
{
|
|
"method": "Runtime.consoleAPICalled",
|
|
"params": {"type": "warning", "args": [{"value": "meh"}]},
|
|
},
|
|
{
|
|
"method": "Runtime.exceptionThrown",
|
|
"params": {
|
|
"exceptionDetails": {
|
|
"text": "Uncaught",
|
|
"exception": {"description": "Error: x\n at y"},
|
|
}
|
|
},
|
|
},
|
|
{
|
|
"method": "Runtime.exceptionThrown",
|
|
"params": {"exceptionDetails": {"text": "Uncaught"}},
|
|
},
|
|
{
|
|
"method": "Log.entryAdded",
|
|
"params": {"entry": {"level": "error", "text": "404"}},
|
|
},
|
|
{
|
|
"method": "Log.entryAdded",
|
|
"params": {"entry": {"level": "warning", "text": "webgl"}},
|
|
},
|
|
{
|
|
"method": "Log.entryAdded",
|
|
"params": {"entry": {"level": "info", "text": "fine"}},
|
|
},
|
|
{"method": "Page.loadEventFired", "params": {}},
|
|
]
|
|
|
|
|
|
def test_console_lists_errors_and_optionally_warnings() -> None:
|
|
page = FakePage()
|
|
out, errors = actions.console(cdp.Session(page.ws(EVENTS)))
|
|
assert errors == 4
|
|
assert out.splitlines() == [
|
|
"error: bad Obj",
|
|
"error: Uncaught Error: x",
|
|
"error: Uncaught",
|
|
"error: 404",
|
|
]
|
|
out, _ = actions.console(cdp.Session(page.ws(EVENTS)), limit=2, warnings=True)
|
|
assert out.splitlines() == ["error: 404", "warning: webgl"]
|
|
assert actions.console(cdp.Session(page.ws([]))) == ("no console errors", 0)
|
|
|
|
|
|
def test_wait_for_row_scopes_the_search() -> None:
|
|
page = FakePage(
|
|
nodes=[
|
|
node(1, "Chapter 2", rect=(0, 100, 1000, 60)),
|
|
node(2, "Mark unread", rect=(900, 110, 40, 40)),
|
|
node(3, "Chapter 1", rect=(0, 160, 1000, 60)),
|
|
node(4, "Mark read", rect=(900, 170, 40, 40)),
|
|
]
|
|
)
|
|
sess, clock = _sess(page)
|
|
hit = actions.wait_for(
|
|
sess, clock, "Mark unread", row="Chapter 2", timeout=1, on_timeout=lambda: "x"
|
|
)
|
|
assert hit.startswith("found #2")
|
|
with pytest.raises(
|
|
DriveError, match=r'"Mark unread" in row "Chapter 1" not present'
|
|
):
|
|
actions.wait_for(
|
|
sess,
|
|
clock,
|
|
"Mark unread",
|
|
row="Chapter 1",
|
|
timeout=1,
|
|
on_timeout=lambda: "x",
|
|
)
|