agent-zero/tests/test_webui_extension_surfaces.py
Alessandro 4ff3244ce6 Add browser annotate mode
Add Codex-inspired annotation UI to the built-in Browser surfaces, including the Annotate toggle, Cmd/Ctrl+. shortcut, selection overlay, inline comments, and batch Draft to chat / Send now actions.

Wire browser_viewer_annotation through the WebSocket and runtime layers, and expose safe DOM metadata extraction for clicked elements and selected areas without leaking password/value data.

Expand regression coverage for the Browser UI, annotation dispatch, runtime helper exposure, prompt formatting, and WebUI extension surface harness behavior.
2026-04-26 23:57:48 +02:00

168 lines
6.8 KiB
Python

from __future__ import annotations
import sys
import tempfile
import threading
from contextlib import contextmanager
from pathlib import Path
from types import SimpleNamespace
from typing import Iterator
import pytest
from flask import Flask
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
class _TestAgentContext:
@staticmethod
def get(context_id):
return None
sys.modules.setdefault("agent", SimpleNamespace(AgentContext=_TestAgentContext))
from api.load_webui_extensions import LoadWebuiExtensions
SURFACE_SCENARIOS: list[tuple[str, str]] = [
("sidebar-start", "webui/components/sidebar/left-sidebar.html"),
("sidebar-end", "webui/components/sidebar/left-sidebar.html"),
("sidebar-top-wrapper-start", "webui/components/sidebar/top-section/sidebar-top.html"),
("sidebar-top-wrapper-end", "webui/components/sidebar/top-section/sidebar-top.html"),
("sidebar-quick-actions-main-start", "webui/components/sidebar/top-section/quick-actions.html"),
("sidebar-quick-actions-main-end", "webui/components/sidebar/top-section/quick-actions.html"),
("sidebar-quick-actions-dropdown-start", "webui/components/sidebar/top-section/quick-actions.html"),
("sidebar-quick-actions-dropdown-end", "webui/components/sidebar/top-section/quick-actions.html"),
("sidebar-chats-list-start", "webui/components/sidebar/chats/chats-list.html"),
("sidebar-chats-list-end", "webui/components/sidebar/chats/chats-list.html"),
("sidebar-tasks-list-start", "webui/components/sidebar/tasks/tasks-list.html"),
("sidebar-tasks-list-end", "webui/components/sidebar/tasks/tasks-list.html"),
("sidebar-bottom-wrapper-start", "webui/components/sidebar/bottom/sidebar-bottom.html"),
("sidebar-bottom-wrapper-end", "webui/components/sidebar/bottom/sidebar-bottom.html"),
("chat-input-start", "webui/components/chat/input/chat-bar.html"),
("chat-input-end", "webui/components/chat/input/chat-bar.html"),
("chat-input-progress-start", "webui/components/chat/input/progress.html"),
("chat-input-progress-end", "webui/components/chat/input/progress.html"),
("chat-input-box-start", "webui/components/chat/input/chat-bar-input.html"),
("chat-input-box-end", "webui/components/chat/input/chat-bar-input.html"),
("chat-input-bottom-actions-start", "webui/components/chat/input/bottom-actions-bar.html"),
("chat-input-bottom-actions-end", "webui/components/chat/input/bottom-actions-bar.html"),
("chat-top-start", "webui/components/chat/top-section/chat-top.html"),
("chat-top-end", "webui/components/chat/top-section/chat-top.html"),
("welcome-screen-start", "webui/components/welcome/welcome-screen.html"),
("welcome-screen-end", "webui/components/welcome/welcome-screen.html"),
("welcome-actions-start", "webui/components/welcome/welcome-screen.html"),
("welcome-actions-end", "webui/components/welcome/welcome-screen.html"),
("welcome-banners-start", "webui/components/welcome/welcome-screen.html"),
("welcome-banners-end", "webui/components/welcome/welcome-screen.html"),
("plugins-list-dropdown-start", "webui/components/plugins/list/plugin-list.html"),
("plugins-list-dropdown-end", "webui/components/plugins/list/plugin-list.html"),
("modal-shell-start", "webui/js/modals.js"),
("modal-shell-end", "webui/js/modals.js"),
("right-canvas-shell-start", "webui/components/canvas/right-canvas.html"),
("right-canvas-tabs-start", "webui/components/canvas/right-canvas.html"),
("right-canvas-tabs-end", "webui/components/canvas/right-canvas.html"),
("right-canvas-toolbar-start", "webui/components/canvas/right-canvas.html"),
("right-canvas-toolbar-end", "webui/components/canvas/right-canvas.html"),
("right-canvas-panels", "webui/components/canvas/right-canvas.html"),
("right-canvas-empty-state", "webui/components/canvas/right-canvas.html"),
("right-canvas-shell-end", "webui/components/canvas/right-canvas.html"),
]
def _new_handler() -> LoadWebuiExtensions:
app = Flask("test_webui_extension_surfaces")
app.secret_key = "test-secret"
return LoadWebuiExtensions(app, threading.RLock())
@pytest.fixture
def anyio_backend():
return "asyncio"
def _assert_surface_anchor_in_template(surface: str, template_rel_path: str) -> None:
template_path = PROJECT_ROOT / template_rel_path
template_html = template_path.read_text(encoding="utf-8")
assert f'<x-extension id="{surface}"></x-extension>' in template_html
@contextmanager
def _temporary_probe_plugin(surface: str) -> Iterator[tuple[str, str]]:
plugins_root = PROJECT_ROOT / "plugins"
with tempfile.TemporaryDirectory(
prefix="tmp_surface_probe_",
dir=plugins_root,
) as temp_plugin_dir:
plugin_id = Path(temp_plugin_dir).name
(Path(temp_plugin_dir) / "plugin.yaml").write_text(
(
f"name: {plugin_id}\n"
f"title: {plugin_id}\n"
"description: Temporary WebUI surface probe.\n"
"version: 0.0.0\n"
"always_enabled: false\n"
),
encoding="utf-8",
)
from helpers import cache
cache.clear("*(plugins)*")
probe_file = (
Path(temp_plugin_dir)
/ "extensions"
/ "webui"
/ surface
/ "surface-probe.html"
)
probe_file.parent.mkdir(parents=True, exist_ok=True)
probe_file.write_text(
(
"<div x-data "
f'data-surface-probe="{surface}" '
f'data-plugin-id="{plugin_id}"></div>'
),
encoding="utf-8",
)
try:
yield plugin_id, probe_file.name
finally:
cache.clear("*(plugins)*")
@pytest.mark.anyio
@pytest.mark.parametrize(
("surface", "template_rel_path"),
SURFACE_SCENARIOS,
ids=[scenario[0] for scenario in SURFACE_SCENARIOS],
)
async def test_webui_surface_extension_point_end_to_end(
surface: str,
template_rel_path: str,
) -> None:
_assert_surface_anchor_in_template(surface, template_rel_path)
with _temporary_probe_plugin(surface) as (plugin_id, probe_file_name):
payload = await _new_handler().process(
{"extension_point": surface, "filters": ["*.html"]},
None,
)
assert isinstance(payload, dict)
extensions = payload.get("extensions", [])
expected_suffix = (
f"{plugin_id}/extensions/webui/{surface}/{probe_file_name}"
)
extension_paths = [
str(
extension.get("path", "")
if isinstance(extension, dict)
else extension
).replace("\\", "/")
for extension in extensions
]
assert any(path.endswith(expected_suffix) for path in extension_paths)