The in-app browser was a one-way mirror. open_preview put a page in the pane
and read_preview read its text back, but nothing could touch it. A click meant
falling back to the browser_* tools, which drive a separate Chromium the user
cannot see — so "log into this and pull my invoices" happened in a different
browser from the one on screen, with none of the sessions the user is already
signed into.
Four pieces, and they only make sense together:
· an in-page engine that inventories what is interactable and performs the
verb, injected as source because it has to run inside the guest page;
· the preview.act.request bridge from the gateway into the pane;
· drive_preview, for acting: elements, click, type, scroll, press, and the
pane's own back/forward/reload;
· annotate_preview, for marking without acting.
Those last two started as one tool doing two unrelated jobs. Leaving a mark is
not an action — it outlives the turn that drew it — so it gets its own verb,
and the interaction verb gets a name that says what it does.
Gating is the existing surface rule: desktop_ui folds in on session
source: 'desktop', and the bridge refuses to act for a background session, so a
turn running behind the user's back cannot reach into the page they are working
in.
Two details worth a reviewer's attention. Typing assigns through the
prototype's value setter, because React shadows value with its own accessor and
ignores an input event whose value it believes it already wrote — a plain
el.value = … types into a field that snaps back on the next render. And
clicking replays the pointer/mouse pair before activation, because frameworks
bind to mousedown as often as to click.
89 lines
2.7 KiB
Python
89 lines
2.7 KiB
Python
"""Tests for the GUI-surface ``annotate_preview`` tool."""
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from tools import annotate_preview_tool as an
|
|
from tools.registry import registry
|
|
|
|
|
|
def test_lives_in_the_gui_surface_toolset(monkeypatch):
|
|
"""Scoped by toolset, not by the backend's env — same as its siblings."""
|
|
monkeypatch.delenv("HERMES_DESKTOP", raising=False)
|
|
entry = registry.get_entry("annotate_preview")
|
|
|
|
assert entry is not None
|
|
assert entry.toolset == "desktop_ui"
|
|
assert entry.check_fn is None
|
|
|
|
|
|
def test_requires_callback():
|
|
result = json.loads(an.annotate_preview_tool(ref="@e1", callback=None))
|
|
assert "desktop" in result["error"]
|
|
|
|
|
|
def test_rejects_an_unknown_action():
|
|
result = json.loads(an.annotate_preview_tool(action="scribble", callback=lambda _p: "{}"))
|
|
assert "action must be one of" in result["error"]
|
|
|
|
|
|
@pytest.mark.parametrize("verb", ["add", "remove"])
|
|
def test_marking_one_thing_needs_a_target(verb):
|
|
"""A mark with nothing to attach to is a mistake worth naming early."""
|
|
result = json.loads(an.annotate_preview_tool(action=verb, callback=lambda _p: "{}"))
|
|
assert "ref" in result["error"]
|
|
|
|
|
|
def test_speaks_the_renderer_s_verbs():
|
|
"""The overlay knows pin/unpin; the model gets words it can guess at."""
|
|
sent = []
|
|
|
|
def cb(payload):
|
|
sent.append(payload)
|
|
return json.dumps({"acted": "pinned Buy now", "success": True})
|
|
|
|
an.annotate_preview_tool(action="add", ref="@e4", label="cheapest", callback=cb)
|
|
an.annotate_preview_tool(action="remove", ref="@e4", callback=cb)
|
|
|
|
assert sent[0] == {"action": "pin", "ref": "@e4", "text": "cheapest"}
|
|
assert sent[1] == {"action": "unpin", "ref": "@e4"}
|
|
|
|
|
|
def test_clear_sends_no_target_so_the_overlay_drops_them_all():
|
|
"""`clear` IS an untargeted unpin — a ref left on it would take down one."""
|
|
sent = []
|
|
|
|
def cb(payload):
|
|
sent.append(payload)
|
|
return json.dumps({"acted": "cleared every pin", "success": True})
|
|
|
|
an.annotate_preview_tool(action="clear", ref="@e4", selector="a", callback=cb)
|
|
|
|
assert sent == [{"action": "unpin"}]
|
|
|
|
|
|
def test_defaults_to_adding():
|
|
sent = []
|
|
|
|
def cb(payload):
|
|
sent.append(payload)
|
|
return json.dumps({"success": True})
|
|
|
|
an.annotate_preview_tool(ref="@e2", callback=cb)
|
|
|
|
assert sent[0]["action"] == "pin"
|
|
|
|
|
|
def test_passes_the_renderer_s_answer_straight_through():
|
|
out = json.loads(
|
|
an.annotate_preview_tool(ref="@e1", callback=lambda _p: json.dumps({"acted": "pinned Save", "success": True}))
|
|
)
|
|
|
|
assert out == {"acted": "pinned Save", "success": True}
|
|
|
|
|
|
def test_reports_a_silent_bridge():
|
|
result = json.loads(an.annotate_preview_tool(ref="@e1", callback=lambda _p: ""))
|
|
assert "open_preview" in result["error"]
|