File size: 5,258 Bytes
6e4eb30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
from __future__ import annotations

import asyncio
import json

import pytest

from browser_core import BrowserBridgeError
from javascript_core import run_javascript
from keyboard_core import press_key
from pointer_core import mouse_click, mouse_down, mouse_up


class _FakeKeyboard:
    def __init__(self) -> None:
        self.pressed: list[str] = []

    async def press(self, key: str) -> None:
        self.pressed.append(key)


class _FakeMouse:
    def __init__(self) -> None:
        self.events: list[tuple[object, ...]] = []

    async def move(self, x: int, y: int) -> None:
        self.events.append(("move", x, y))

    async def down(self, *, button: str) -> None:
        self.events.append(("down", button))

    async def up(self, *, button: str) -> None:
        self.events.append(("up", button))

    async def click(self, x: int, y: int, *, button: str, click_count: int) -> None:
        self.events.append(("click", x, y, button, click_count))


class _FakeLocator:
    async def count(self) -> int:
        return 0


class _FakePage:
    def __init__(self, target: dict[str, object], js_result: object = None) -> None:
        self.target = target
        self.js_result = {"answer": 42} if js_result is None else js_result
        self.url = "https://example.com/test"
        self.viewport_size = {"width": 1280, "height": 900}
        self.keyboard = _FakeKeyboard()
        self.mouse = _FakeMouse()

    async def evaluate(self, source: str, _arg: object = None) -> object:
        if "document.activeElement" in source:
            return self.target
        encoded = json.dumps(self.js_result)
        return {"ok": True, "result_json": encoded, "result_bytes": len(encoded.encode("utf-8"))}

    def locator(self, _selector: str) -> _FakeLocator:
        return _FakeLocator()


class _FakeSession:
    def __init__(self, page: _FakePage) -> None:
        self.session_id = "session-1"
        self.page = page
        self.lock = asyncio.Lock()


class _FakeManager:
    def __init__(self, page: _FakePage) -> None:
        self.session = _FakeSession(page)

    async def get(self, _owner: str, _session_id: str, *, touch: bool = False) -> _FakeSession:
        return self.session

    async def revalidate_locked_session(self, _owner: str, _session_id: str, _session: _FakeSession) -> None:
        return None

    async def _sync_page(self, session: _FakeSession) -> _FakePage:
        return session.page


def _target(*, tag: str, type: str = "", role: str = "", label: str = "", content_editable: bool = False) -> dict[str, object]:
    return {
        "present": True,
        "tag": tag,
        "type": type,
        "role": role,
        "label": label,
        "autocomplete": "",
        "content_editable": content_editable,
    }


def test_enter_is_available_for_search_but_not_generic_text_input() -> None:
    async def exercise() -> None:
        search_page = _FakePage(_target(tag="input", type="search", label="Search"))
        search_result = await press_key(_FakeManager(search_page), "owner", "session-1", "Enter")
        assert search_result["status"] == "BROWSER_KEY_PRESSED"
        assert search_page.keyboard.pressed == ["Enter"]

        text_page = _FakePage(_target(tag="input", type="text", label="Message"))
        with pytest.raises(BrowserBridgeError) as raised:
            await press_key(_FakeManager(text_page), "owner", "session-1", "Enter")
        assert raised.value.code == "KEY_ENTER_TARGET_BLOCKED"

    asyncio.run(exercise())


def test_javascript_requires_action_confirmation_and_redacts_secret_api_path() -> None:
    async def exercise() -> None:
        page = _FakePage(_target(tag="textarea", label="Note"))
        manager = _FakeManager(page)
        with pytest.raises(BrowserBridgeError) as unconfirmed:
            await run_javascript(manager, "owner", "session-1", "return { answer: 42 };", user_confirmed_javascript_action=False)
        assert unconfirmed.value.code == "JAVASCRIPT_ACTION_CONFIRMATION_REQUIRED"

        with pytest.raises(BrowserBridgeError) as secret:
            await run_javascript(manager, "owner", "session-1", "return document.cookie;", user_confirmed_javascript_action=True)
        assert secret.value.code == "JAVASCRIPT_SECRET_OR_NETWORK_API_BLOCKED"

        result = await run_javascript(manager, "owner", "session-1", "return { answer: 42 };", user_confirmed_javascript_action=True)
        assert result["status"] == "BROWSER_JAVASCRIPT_COMPLETED"
        assert result["result"] == {"answer": 42}
        assert result["user_edge_profile_used"] is False

    asyncio.run(exercise())


def test_raw_canvas_pointer_no_longer_needs_repeated_confirmation() -> None:
    async def exercise() -> None:
        page = _FakePage(_target(tag="canvas", label="Chart"))
        manager = _FakeManager(page)
        clicked = await mouse_click(manager, "owner", "session-1", 100, 80)
        assert clicked["status"] == "BROWSER_POINTER_CLICKED"
        assert page.mouse.events == [("click", 100, 80, "left", 1)]

        await mouse_down(manager, "owner", "session-1")
        await mouse_up(manager, "owner", "session-1")
        assert page.mouse.events[-2:] == [("down", "left"), ("up", "left")]

    asyncio.run(exercise())