Files
eepp/projects/scripts/tests/test_eepp_inspect.py
T
Martín Lucas Golini 4ffba20d07 feat(ui): add runtime inspector and fix HTML layout regressions
Add an opt-in TCP/NDJSON inspector for running eepp applications. It
provides authenticated, UI-thread-safe access to windows, scenes, and
widgets, including nested WebView documents. The v1 protocol supports
queries, trees, properties, focus, input actions, watches, screenshots,
and composed batches with result references and deferred execution.

Add a standard-library Python CLI, protocol documentation, build
entries, and integration tests. The listener remains disabled unless
explicitly enabled.

Fix HTML compatibility issues found while debugging NewsBlur:
- Let explicit flex minimums, including min-width: 0, replace the
  automatic content minimum.
- Measure flex lines against the container content box when padding
  and explicit dimensions are present.
- Keep promoted positioned descendants of hidden ancestors out of
  the paint list.
2026-09-28 01:15:39 -03:00

122 lines
4.9 KiB
Python

"""Transport and CLI smoke tests using a small fake NDJSON inspector."""
import json
import pathlib
import socket
import subprocess
import sys
import threading
import unittest
CLIENT = pathlib.Path(__file__).resolve().parents[1] / "eepp-inspect.py"
class FakeServer:
def __init__(self, handler):
self.listener = socket.socket()
self.listener.bind(("127.0.0.1", 0))
self.listener.listen(1)
self.port = self.listener.getsockname()[1]
self.handler = handler
self.error = None
self.thread = threading.Thread(target=self.run, daemon=True)
self.thread.start()
def run(self):
try:
with self.listener:
conn, _ = self.listener.accept()
with conn, conn.makefile("r", encoding="utf-8") as lines:
connect = json.loads(lines.readline())
assert connect["method"] == "session.connect"
assert connect["params"]["token"] == "test-token"
conn.sendall(b'{"id":1,"result":{"protocolVersion":1}}\n')
self.handler(conn, lines)
except Exception as exc:
self.error = exc
def join(self):
self.thread.join(timeout=5)
if self.error:
raise self.error
assert not self.thread.is_alive(), "fake server did not finish"
def invoke(server, *args, input_text=None):
return subprocess.run(
[sys.executable, str(CLIENT), "--port", str(server.port), "--token", "test-token", *args],
input=input_text, text=True, capture_output=True, timeout=5,
)
class InspectorCliTests(unittest.TestCase):
def test_contexts_emits_compact_json(self):
def handler(conn, lines):
request = json.loads(lines.readline())
self.assertEqual(request["method"], "ui.contexts")
conn.sendall((json.dumps({"id": request["id"], "result": {"scenes": []}}) + "\n").encode())
server = FakeServer(handler)
result = invoke(server, "contexts")
server.join()
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(json.loads(result.stdout)["result"]["scenes"], [])
self.assertEqual(result.stdout.count("\n"), 1)
def test_batch_reads_stdin_and_preserves_commands(self):
def handler(conn, lines):
request = json.loads(lines.readline())
self.assertEqual(request["method"], "session.batch")
self.assertFalse(request["params"]["commands"][0]["return"])
conn.sendall((json.dumps({"id": request["id"], "result": {"results": {}}}) + "\n").encode())
server = FakeServer(handler)
definition = '{"commands":[{"method":"ui.query","return":false,"params":{"selector":"*"}}]}'
result = invoke(server, "batch", "-", input_text=definition)
server.join()
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(json.loads(result.stdout)["result"]["results"], {})
def test_screenshot_sends_window_rect_and_format(self):
def handler(conn, lines):
request = json.loads(lines.readline())
self.assertEqual(request["method"], "ui.screenshot")
self.assertEqual(request["params"], {
"window": "win:2", "rect": [5, 6, 30, 20], "format": "qoi"})
conn.sendall((json.dumps({"id": request["id"], "result": {
"path": "/tmp/example.qoi", "format": "qoi"}}) + "\n").encode())
server = FakeServer(handler)
result = invoke(server, "screenshot", "--window", "win:2", "--rect",
"5", "6", "30", "20", "--format", "qoi")
server.join()
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(json.loads(result.stdout)["result"]["path"], "/tmp/example.qoi")
def test_session_streams_events_between_pipelined_responses(self):
def handler(conn, lines):
first = json.loads(lines.readline())
second = json.loads(lines.readline())
self.assertEqual(first["method"], "ui.contexts")
self.assertEqual(second["method"], "ui.focus")
messages = [
{"method": "watch.changed", "params": {"sequence": 1}},
{"id": second["id"], "result": {"widget": None}},
{"id": first["id"], "result": {"scenes": []}},
]
conn.sendall("".join(json.dumps(message) + "\n" for message in messages).encode())
server = FakeServer(handler)
requests = '{"method":"ui.contexts"}\n{"method":"ui.focus"}\n'
result = invoke(server, "session", input_text=requests)
server.join()
self.assertEqual(result.returncode, 0, result.stderr)
messages = [json.loads(line) for line in result.stdout.splitlines()]
self.assertEqual(messages[0]["method"], "watch.changed")
self.assertEqual({message.get("id") for message in messages[1:]}, {2, 3})
if __name__ == "__main__":
unittest.main()