browser-control-and-automation-cli 0.1.1.dev3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- browser_cli/__init__.py +7 -0
- browser_cli/actions/__init__.py +5 -0
- browser_cli/actions/cli_specs.py +868 -0
- browser_cli/agent_scope/__init__.py +14 -0
- browser_cli/automation/__init__.py +11 -0
- browser_cli/automation/api/__init__.py +5 -0
- browser_cli/automation/api/server.py +321 -0
- browser_cli/automation/hooks.py +42 -0
- browser_cli/automation/loader.py +134 -0
- browser_cli/automation/models.py +127 -0
- browser_cli/automation/persistence/__init__.py +5 -0
- browser_cli/automation/persistence/store.py +682 -0
- browser_cli/automation/projections.py +279 -0
- browser_cli/automation/publisher.py +153 -0
- browser_cli/automation/scheduler/__init__.py +5 -0
- browser_cli/automation/scheduler/schedule.py +113 -0
- browser_cli/automation/service/__init__.py +8 -0
- browser_cli/automation/service/__main__.py +64 -0
- browser_cli/automation/service/client.py +232 -0
- browser_cli/automation/service/runtime.py +388 -0
- browser_cli/automation/toml.py +35 -0
- browser_cli/automation/web/__init__.py +5 -0
- browser_cli/automation/web/assets.py +377 -0
- browser_cli/browser/__init__.py +1 -0
- browser_cli/browser/models.py +44 -0
- browser_cli/browser/network_capture.py +392 -0
- browser_cli/browser/service.py +1519 -0
- browser_cli/browser/session.py +158 -0
- browser_cli/browser/snapshot.py +35 -0
- browser_cli/browser/stealth.py +450 -0
- browser_cli/cli/__init__.py +1 -0
- browser_cli/cli/error_hints.py +26 -0
- browser_cli/cli/main.py +298 -0
- browser_cli/commands/__init__.py +1 -0
- browser_cli/commands/action.py +25 -0
- browser_cli/commands/automation.py +304 -0
- browser_cli/commands/doctor.py +336 -0
- browser_cli/commands/install_skills.py +138 -0
- browser_cli/commands/paths.py +31 -0
- browser_cli/commands/read.py +33 -0
- browser_cli/commands/reload.py +51 -0
- browser_cli/commands/status.py +444 -0
- browser_cli/commands/task.py +71 -0
- browser_cli/constants.py +97 -0
- browser_cli/daemon/__init__.py +1 -0
- browser_cli/daemon/__main__.py +34 -0
- browser_cli/daemon/app.py +941 -0
- browser_cli/daemon/browser_service.py +998 -0
- browser_cli/daemon/client.py +280 -0
- browser_cli/daemon/models.py +63 -0
- browser_cli/daemon/runtime_presentation.py +167 -0
- browser_cli/daemon/server.py +103 -0
- browser_cli/daemon/state.py +15 -0
- browser_cli/daemon/transport.py +77 -0
- browser_cli/drivers/__init__.py +6 -0
- browser_cli/drivers/_extension/__init__.py +1 -0
- browser_cli/drivers/_extension/artifact_actions.py +154 -0
- browser_cli/drivers/_extension/dialog_actions.py +35 -0
- browser_cli/drivers/_extension/input_actions.py +63 -0
- browser_cli/drivers/_extension/locator_actions.py +100 -0
- browser_cli/drivers/_extension/observe_actions.py +285 -0
- browser_cli/drivers/_extension/page_actions.py +210 -0
- browser_cli/drivers/_extension/state_actions.py +134 -0
- browser_cli/drivers/base.py +391 -0
- browser_cli/drivers/extension_driver.py +33 -0
- browser_cli/drivers/models.py +21 -0
- browser_cli/drivers/playwright_driver.py +499 -0
- browser_cli/error_codes.py +27 -0
- browser_cli/errors.py +146 -0
- browser_cli/exit_codes.py +9 -0
- browser_cli/extension/__init__.py +32 -0
- browser_cli/extension/protocol.py +222 -0
- browser_cli/extension/session.py +393 -0
- browser_cli/network.py +147 -0
- browser_cli/outputs/__init__.py +1 -0
- browser_cli/outputs/json.py +10 -0
- browser_cli/outputs/render.py +7 -0
- browser_cli/packaged_skills/__init__.py +1 -0
- browser_cli/packaged_skills/browser-cli-converge/SKILL.md +62 -0
- browser_cli/packaged_skills/browser-cli-delivery/SKILL.md +61 -0
- browser_cli/packaged_skills/browser-cli-explore/SKILL.md +77 -0
- browser_cli/profiles/__init__.py +1 -0
- browser_cli/profiles/discovery.py +185 -0
- browser_cli/refs/__init__.py +17 -0
- browser_cli/refs/generator.py +386 -0
- browser_cli/refs/models.py +127 -0
- browser_cli/refs/registry.py +51 -0
- browser_cli/refs/resolver.py +214 -0
- browser_cli/tabs/__init__.py +5 -0
- browser_cli/tabs/registry.py +283 -0
- browser_cli/task_runtime/__init__.py +31 -0
- browser_cli/task_runtime/client.py +107 -0
- browser_cli/task_runtime/entrypoint.py +110 -0
- browser_cli/task_runtime/errors.py +17 -0
- browser_cli/task_runtime/flow.py +171 -0
- browser_cli/task_runtime/models.py +97 -0
- browser_cli/task_runtime/read.py +71 -0
- browser_cli/task_runtime/templates.py +58 -0
- browser_control_and_automation_cli-0.1.1.dev3.dist-info/METADATA +340 -0
- browser_control_and_automation_cli-0.1.1.dev3.dist-info/RECORD +104 -0
- browser_control_and_automation_cli-0.1.1.dev3.dist-info/WHEEL +5 -0
- browser_control_and_automation_cli-0.1.1.dev3.dist-info/entry_points.txt +2 -0
- browser_control_and_automation_cli-0.1.1.dev3.dist-info/licenses/LICENSE +21 -0
- browser_control_and_automation_cli-0.1.1.dev3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Unix-socket transport helpers for the daemon."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import socket
|
|
9
|
+
import stat
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from browser_cli.constants import get_app_paths
|
|
14
|
+
|
|
15
|
+
DAEMON_RUNTIME_VERSION = "2026-04-10-dual-driver-extension-v1"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def ensure_run_dir() -> Path:
|
|
19
|
+
run_dir = get_app_paths().run_dir
|
|
20
|
+
run_dir.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
with contextlib.suppress(OSError):
|
|
22
|
+
os.chmod(run_dir, 0o700)
|
|
23
|
+
return run_dir
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def write_run_info(info: dict[str, Any]) -> None:
|
|
27
|
+
ensure_run_dir()
|
|
28
|
+
run_info_path = get_app_paths().run_info_path
|
|
29
|
+
tmp_path = run_info_path.with_suffix(".tmp")
|
|
30
|
+
tmp_path.write_text(json.dumps(info), encoding="utf-8")
|
|
31
|
+
tmp_path.replace(run_info_path)
|
|
32
|
+
with contextlib.suppress(OSError):
|
|
33
|
+
os.chmod(run_info_path, 0o600)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def read_run_info() -> dict[str, Any] | None:
|
|
37
|
+
run_info_path = get_app_paths().run_info_path
|
|
38
|
+
try:
|
|
39
|
+
return json.loads(run_info_path.read_text(encoding="utf-8"))
|
|
40
|
+
except (FileNotFoundError, OSError, json.JSONDecodeError):
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def remove_run_info() -> None:
|
|
45
|
+
with contextlib.suppress(OSError):
|
|
46
|
+
get_app_paths().run_info_path.unlink()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def safe_remove_socket(path: Path | None = None) -> None:
|
|
50
|
+
socket_path = path or get_app_paths().socket_path
|
|
51
|
+
try:
|
|
52
|
+
st = socket_path.stat()
|
|
53
|
+
except FileNotFoundError:
|
|
54
|
+
return
|
|
55
|
+
if not stat.S_ISSOCK(st.st_mode):
|
|
56
|
+
raise RuntimeError(f"Refusing to remove non-socket path: {socket_path}")
|
|
57
|
+
if hasattr(os, "getuid") and st.st_uid != os.getuid():
|
|
58
|
+
raise PermissionError(
|
|
59
|
+
f"Socket path {socket_path} is owned by uid={st.st_uid}, current uid={os.getuid()}"
|
|
60
|
+
)
|
|
61
|
+
try:
|
|
62
|
+
socket_path.unlink()
|
|
63
|
+
except FileNotFoundError:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def probe_socket(path: Path | None = None) -> bool:
|
|
68
|
+
socket_path = str(path or get_app_paths().socket_path)
|
|
69
|
+
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
70
|
+
try:
|
|
71
|
+
client.settimeout(1.0)
|
|
72
|
+
client.connect(socket_path)
|
|
73
|
+
return True
|
|
74
|
+
except OSError:
|
|
75
|
+
return False
|
|
76
|
+
finally:
|
|
77
|
+
client.close()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Internal helpers for the extension-backed browser driver."""
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ExtensionDriverArtifactActionsMixin:
|
|
8
|
+
async def screenshot(
|
|
9
|
+
self, page_id: str, *, path: str, full_page: bool = False
|
|
10
|
+
) -> dict[str, Any]:
|
|
11
|
+
payload = await self._page_command(
|
|
12
|
+
page_id,
|
|
13
|
+
"screenshot",
|
|
14
|
+
{"full_page": full_page},
|
|
15
|
+
)
|
|
16
|
+
output_path = await self._materialize_single_artifact(
|
|
17
|
+
payload,
|
|
18
|
+
page_id=page_id,
|
|
19
|
+
path=path,
|
|
20
|
+
suffix=".png",
|
|
21
|
+
artifact_kind="screenshot",
|
|
22
|
+
inline_key="data_base64",
|
|
23
|
+
)
|
|
24
|
+
return {"page_id": page_id, "path": output_path, "full_page": full_page}
|
|
25
|
+
|
|
26
|
+
async def save_pdf(self, page_id: str, *, path: str) -> dict[str, Any]:
|
|
27
|
+
payload = await self._page_command(page_id, "pdf", {})
|
|
28
|
+
output_path = await self._materialize_single_artifact(
|
|
29
|
+
payload,
|
|
30
|
+
page_id=page_id,
|
|
31
|
+
path=path,
|
|
32
|
+
suffix=".pdf",
|
|
33
|
+
artifact_kind="pdf",
|
|
34
|
+
inline_key="data_base64",
|
|
35
|
+
)
|
|
36
|
+
return {"page_id": page_id, "path": output_path}
|
|
37
|
+
|
|
38
|
+
async def start_tracing(
|
|
39
|
+
self,
|
|
40
|
+
page_id: str,
|
|
41
|
+
*,
|
|
42
|
+
screenshots: bool = True,
|
|
43
|
+
snapshots: bool = True,
|
|
44
|
+
sources: bool = False,
|
|
45
|
+
) -> dict[str, Any]:
|
|
46
|
+
payload = await self._page_command(
|
|
47
|
+
page_id,
|
|
48
|
+
"trace-start",
|
|
49
|
+
{"screenshots": screenshots, "snapshots": snapshots, "sources": sources},
|
|
50
|
+
)
|
|
51
|
+
return {"page_id": page_id, **payload}
|
|
52
|
+
|
|
53
|
+
async def add_trace_chunk(self, page_id: str, *, title: str | None = None) -> dict[str, Any]:
|
|
54
|
+
payload = await self._page_command(page_id, "trace-chunk", {"title": title})
|
|
55
|
+
return {"page_id": page_id, **payload}
|
|
56
|
+
|
|
57
|
+
async def stop_tracing(self, page_id: str, *, path: str | None = None) -> dict[str, Any]:
|
|
58
|
+
payload = await self._page_command(page_id, "trace-stop", {"path": path})
|
|
59
|
+
output_path = await self._materialize_single_artifact(
|
|
60
|
+
payload,
|
|
61
|
+
page_id=page_id,
|
|
62
|
+
path=path,
|
|
63
|
+
suffix=".zip",
|
|
64
|
+
artifact_kind="trace",
|
|
65
|
+
)
|
|
66
|
+
return {"page_id": page_id, "path": output_path}
|
|
67
|
+
|
|
68
|
+
async def start_video(
|
|
69
|
+
self, page_id: str, *, width: int | None = None, height: int | None = None
|
|
70
|
+
) -> dict[str, Any]:
|
|
71
|
+
payload = await self._page_command(
|
|
72
|
+
page_id, "video-start", {"width": width, "height": height}
|
|
73
|
+
)
|
|
74
|
+
return {"page_id": page_id, **payload}
|
|
75
|
+
|
|
76
|
+
async def stop_video(self, page_id: str, *, path: str | None = None) -> dict[str, Any]:
|
|
77
|
+
payload = await self._page_command(page_id, "video-stop", {"path": path})
|
|
78
|
+
return {"page_id": page_id, **payload}
|
|
79
|
+
|
|
80
|
+
async def _materialize_single_artifact(
|
|
81
|
+
self,
|
|
82
|
+
payload: dict[str, Any],
|
|
83
|
+
*,
|
|
84
|
+
page_id: str,
|
|
85
|
+
path: str | None,
|
|
86
|
+
suffix: str,
|
|
87
|
+
artifact_kind: str,
|
|
88
|
+
inline_key: str | None = None,
|
|
89
|
+
) -> str:
|
|
90
|
+
artifacts = list(payload.get("_artifacts") or [])
|
|
91
|
+
if artifacts:
|
|
92
|
+
for artifact in artifacts:
|
|
93
|
+
if str(artifact.get("artifact_kind") or "") != artifact_kind:
|
|
94
|
+
continue
|
|
95
|
+
resolved = self._resolve_artifact_output_path(
|
|
96
|
+
artifact,
|
|
97
|
+
page_id=page_id,
|
|
98
|
+
path=path,
|
|
99
|
+
suffix=suffix,
|
|
100
|
+
)
|
|
101
|
+
self._write_artifact_bytes(artifact, resolved)
|
|
102
|
+
return str(resolved)
|
|
103
|
+
if inline_key and payload.get(inline_key):
|
|
104
|
+
output_path = self._resolve_output_path(path, page_id=page_id, suffix=suffix)
|
|
105
|
+
output_path.write_bytes(
|
|
106
|
+
base64.b64decode(str(payload.get(inline_key) or "").encode("ascii"))
|
|
107
|
+
)
|
|
108
|
+
return str(output_path)
|
|
109
|
+
if path:
|
|
110
|
+
return str(self._resolve_output_path(path, page_id=page_id, suffix=suffix))
|
|
111
|
+
raise RuntimeError(f"Expected extension artifact kind {artifact_kind}.")
|
|
112
|
+
|
|
113
|
+
async def _materialize_video_artifacts(self, artifacts: list[dict[str, Any]]) -> list[str]:
|
|
114
|
+
paths: list[str] = []
|
|
115
|
+
for artifact in artifacts:
|
|
116
|
+
if str(artifact.get("artifact_kind") or "") != "video":
|
|
117
|
+
continue
|
|
118
|
+
page_id = str(artifact.get("page_id") or "page")
|
|
119
|
+
resolved = self._resolve_artifact_output_path(
|
|
120
|
+
artifact,
|
|
121
|
+
page_id=page_id,
|
|
122
|
+
path=str((artifact.get("metadata") or {}).get("requested_path") or "").strip()
|
|
123
|
+
or None,
|
|
124
|
+
suffix=".webm",
|
|
125
|
+
)
|
|
126
|
+
self._write_artifact_bytes(artifact, resolved)
|
|
127
|
+
paths.append(str(resolved))
|
|
128
|
+
return paths
|
|
129
|
+
|
|
130
|
+
def _resolve_artifact_output_path(
|
|
131
|
+
self,
|
|
132
|
+
artifact: dict[str, Any],
|
|
133
|
+
*,
|
|
134
|
+
page_id: str,
|
|
135
|
+
path: str | None,
|
|
136
|
+
suffix: str,
|
|
137
|
+
):
|
|
138
|
+
if path:
|
|
139
|
+
return self._resolve_output_path(path, page_id=page_id, suffix=suffix)
|
|
140
|
+
metadata = dict(artifact.get("metadata") or {})
|
|
141
|
+
requested = str(metadata.get("requested_path") or "").strip()
|
|
142
|
+
if requested:
|
|
143
|
+
return self._resolve_output_path(requested, page_id=page_id, suffix=suffix)
|
|
144
|
+
filename = str(artifact.get("filename") or "").strip()
|
|
145
|
+
if filename:
|
|
146
|
+
return self._resolve_output_path(filename, page_id=page_id, suffix=suffix)
|
|
147
|
+
return self._resolve_output_path(None, page_id=page_id, suffix=suffix)
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def _write_artifact_bytes(artifact: dict[str, Any], output_path) -> None:
|
|
151
|
+
content = artifact.get("content")
|
|
152
|
+
if not isinstance(content, bytes | bytearray):
|
|
153
|
+
raise RuntimeError("Extension artifact payload is missing binary content.")
|
|
154
|
+
output_path.write_bytes(bytes(content))
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ExtensionDriverDialogActionsMixin:
|
|
5
|
+
async def setup_dialog_handler(
|
|
6
|
+
self,
|
|
7
|
+
page_id: str,
|
|
8
|
+
*,
|
|
9
|
+
default_action: str = "accept",
|
|
10
|
+
default_prompt_text: str | None = None,
|
|
11
|
+
) -> dict[str, object]:
|
|
12
|
+
payload = await self._page_command(
|
|
13
|
+
page_id,
|
|
14
|
+
"dialog-setup",
|
|
15
|
+
{"action": default_action, "text": default_prompt_text},
|
|
16
|
+
)
|
|
17
|
+
return {"page_id": page_id, **payload}
|
|
18
|
+
|
|
19
|
+
async def handle_dialog(
|
|
20
|
+
self,
|
|
21
|
+
page_id: str,
|
|
22
|
+
*,
|
|
23
|
+
accept: bool,
|
|
24
|
+
prompt_text: str | None = None,
|
|
25
|
+
) -> dict[str, object]:
|
|
26
|
+
payload = await self._page_command(
|
|
27
|
+
page_id,
|
|
28
|
+
"dialog",
|
|
29
|
+
{"dismiss": not accept, "text": prompt_text},
|
|
30
|
+
)
|
|
31
|
+
return {"page_id": page_id, **payload}
|
|
32
|
+
|
|
33
|
+
async def remove_dialog_handler(self, page_id: str) -> dict[str, object]:
|
|
34
|
+
payload = await self._page_command(page_id, "dialog-remove", {})
|
|
35
|
+
return {"page_id": page_id, **payload}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ExtensionDriverInputActionsMixin:
|
|
5
|
+
async def type_text(
|
|
6
|
+
self, page_id: str, text: str, *, submit: bool = False
|
|
7
|
+
) -> dict[str, object]:
|
|
8
|
+
payload = await self._page_command(page_id, "type", {"text": text, "submit": submit})
|
|
9
|
+
return {"page_id": page_id, **payload}
|
|
10
|
+
|
|
11
|
+
async def press_key(self, page_id: str, key: str) -> dict[str, object]:
|
|
12
|
+
payload = await self._page_command(page_id, "press", {"key": key})
|
|
13
|
+
return {"page_id": page_id, **payload}
|
|
14
|
+
|
|
15
|
+
async def key_down(self, page_id: str, key: str) -> dict[str, object]:
|
|
16
|
+
payload = await self._page_command(page_id, "key-down", {"key": key})
|
|
17
|
+
return {"page_id": page_id, **payload}
|
|
18
|
+
|
|
19
|
+
async def key_up(self, page_id: str, key: str) -> dict[str, object]:
|
|
20
|
+
payload = await self._page_command(page_id, "key-up", {"key": key})
|
|
21
|
+
return {"page_id": page_id, **payload}
|
|
22
|
+
|
|
23
|
+
async def wheel(self, page_id: str, *, dx: int = 0, dy: int = 700) -> dict[str, object]:
|
|
24
|
+
payload = await self._page_command(page_id, "scroll", {"dx": dx, "dy": dy})
|
|
25
|
+
return {"page_id": page_id, **payload}
|
|
26
|
+
|
|
27
|
+
async def mouse_move(self, page_id: str, *, x: int, y: int) -> dict[str, object]:
|
|
28
|
+
payload = await self._page_command(page_id, "mouse-move", {"x": x, "y": y})
|
|
29
|
+
return {"page_id": page_id, **payload}
|
|
30
|
+
|
|
31
|
+
async def mouse_click(
|
|
32
|
+
self,
|
|
33
|
+
page_id: str,
|
|
34
|
+
*,
|
|
35
|
+
x: int,
|
|
36
|
+
y: int,
|
|
37
|
+
button: str = "left",
|
|
38
|
+
count: int = 1,
|
|
39
|
+
) -> dict[str, object]:
|
|
40
|
+
payload = await self._page_command(
|
|
41
|
+
page_id,
|
|
42
|
+
"mouse-click",
|
|
43
|
+
{"x": x, "y": y, "button": button, "count": count},
|
|
44
|
+
)
|
|
45
|
+
return {"page_id": page_id, **payload}
|
|
46
|
+
|
|
47
|
+
async def mouse_drag(
|
|
48
|
+
self, page_id: str, *, x1: int, y1: int, x2: int, y2: int
|
|
49
|
+
) -> dict[str, object]:
|
|
50
|
+
payload = await self._page_command(
|
|
51
|
+
page_id,
|
|
52
|
+
"mouse-drag",
|
|
53
|
+
{"x1": x1, "y1": y1, "x2": x2, "y2": y2},
|
|
54
|
+
)
|
|
55
|
+
return {"page_id": page_id, **payload}
|
|
56
|
+
|
|
57
|
+
async def mouse_down(self, page_id: str, *, button: str = "left") -> dict[str, object]:
|
|
58
|
+
payload = await self._page_command(page_id, "mouse-down", {"button": button})
|
|
59
|
+
return {"page_id": page_id, **payload}
|
|
60
|
+
|
|
61
|
+
async def mouse_up(self, page_id: str, *, button: str = "left") -> dict[str, object]:
|
|
62
|
+
payload = await self._page_command(page_id, "mouse-up", {"button": button})
|
|
63
|
+
return {"page_id": page_id, **payload}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from browser_cli.refs.models import LocatorSpec
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ExtensionDriverLocatorActionsMixin:
|
|
9
|
+
async def click(self, page_id: str, locator: LocatorSpec) -> dict[str, Any]:
|
|
10
|
+
await self._locator_command(page_id, "click", locator)
|
|
11
|
+
return {"page_id": page_id, "ref": locator.ref, "action": "click"}
|
|
12
|
+
|
|
13
|
+
async def double_click(self, page_id: str, locator: LocatorSpec) -> dict[str, Any]:
|
|
14
|
+
await self._locator_command(page_id, "double-click", locator)
|
|
15
|
+
return {"page_id": page_id, "ref": locator.ref, "action": "double-click"}
|
|
16
|
+
|
|
17
|
+
async def hover(self, page_id: str, locator: LocatorSpec) -> dict[str, Any]:
|
|
18
|
+
await self._locator_command(page_id, "hover", locator)
|
|
19
|
+
return {"page_id": page_id, "ref": locator.ref, "action": "hover"}
|
|
20
|
+
|
|
21
|
+
async def focus(self, page_id: str, locator: LocatorSpec) -> dict[str, Any]:
|
|
22
|
+
await self._locator_command(page_id, "focus", locator)
|
|
23
|
+
return {"page_id": page_id, "ref": locator.ref, "action": "focus"}
|
|
24
|
+
|
|
25
|
+
async def fill(
|
|
26
|
+
self,
|
|
27
|
+
page_id: str,
|
|
28
|
+
locator: LocatorSpec,
|
|
29
|
+
text: str,
|
|
30
|
+
*,
|
|
31
|
+
submit: bool = False,
|
|
32
|
+
) -> dict[str, Any]:
|
|
33
|
+
await self._locator_command(page_id, "fill", locator, {"text": text, "submit": submit})
|
|
34
|
+
return {"page_id": page_id, "ref": locator.ref, "filled": True, "submitted": submit}
|
|
35
|
+
|
|
36
|
+
async def select_option(self, page_id: str, locator: LocatorSpec, text: str) -> dict[str, Any]:
|
|
37
|
+
await self._locator_command(page_id, "select", locator, {"text": text})
|
|
38
|
+
return {"page_id": page_id, "ref": locator.ref, "selected": text}
|
|
39
|
+
|
|
40
|
+
async def list_options(self, page_id: str, locator: LocatorSpec) -> dict[str, Any]:
|
|
41
|
+
payload = await self._locator_command(page_id, "options", locator)
|
|
42
|
+
return {"page_id": page_id, "ref": locator.ref, **payload}
|
|
43
|
+
|
|
44
|
+
async def check(self, page_id: str, locator: LocatorSpec, *, checked: bool) -> dict[str, Any]:
|
|
45
|
+
await self._locator_command(page_id, "check", locator, {"checked": checked})
|
|
46
|
+
return {"page_id": page_id, "ref": locator.ref, "checked": checked}
|
|
47
|
+
|
|
48
|
+
async def scroll_to(self, page_id: str, locator: LocatorSpec) -> dict[str, Any]:
|
|
49
|
+
await self._locator_command(page_id, "scroll-to", locator)
|
|
50
|
+
return {"page_id": page_id, "ref": locator.ref, "scrolled": True}
|
|
51
|
+
|
|
52
|
+
async def drag(
|
|
53
|
+
self, page_id: str, start_locator: LocatorSpec, end_locator: LocatorSpec
|
|
54
|
+
) -> dict[str, Any]:
|
|
55
|
+
payload = await self._page_command(
|
|
56
|
+
page_id,
|
|
57
|
+
"drag",
|
|
58
|
+
{"start_locator": start_locator.to_dict(), "end_locator": end_locator.to_dict()},
|
|
59
|
+
)
|
|
60
|
+
return {
|
|
61
|
+
"page_id": page_id,
|
|
62
|
+
"start_ref": start_locator.ref,
|
|
63
|
+
"end_ref": end_locator.ref,
|
|
64
|
+
**payload,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async def upload(self, page_id: str, locator: LocatorSpec, file_path: str) -> dict[str, Any]:
|
|
68
|
+
payload = await self._locator_command(page_id, "upload", locator, {"path": file_path})
|
|
69
|
+
return {"page_id": page_id, "ref": locator.ref, **payload}
|
|
70
|
+
|
|
71
|
+
async def evaluate_on(self, page_id: str, locator: LocatorSpec, code: str) -> dict[str, Any]:
|
|
72
|
+
payload = await self._locator_command(page_id, "eval-on", locator, {"code": code})
|
|
73
|
+
return {"page_id": page_id, "ref": locator.ref, "result": payload.get("result")}
|
|
74
|
+
|
|
75
|
+
async def verify_state(
|
|
76
|
+
self, page_id: str, *, locator: LocatorSpec, state: str
|
|
77
|
+
) -> dict[str, Any]:
|
|
78
|
+
payload = await self._locator_command(page_id, "verify-state", locator, {"state": state})
|
|
79
|
+
return {"page_id": page_id, "ref": locator.ref, **payload}
|
|
80
|
+
|
|
81
|
+
async def verify_value(
|
|
82
|
+
self, page_id: str, *, locator: LocatorSpec, expected: str
|
|
83
|
+
) -> dict[str, Any]:
|
|
84
|
+
payload = await self._locator_command(
|
|
85
|
+
page_id, "verify-value", locator, {"expected": expected}
|
|
86
|
+
)
|
|
87
|
+
return {"page_id": page_id, "ref": locator.ref, **payload}
|
|
88
|
+
|
|
89
|
+
async def _locator_command(
|
|
90
|
+
self,
|
|
91
|
+
page_id: str,
|
|
92
|
+
action: str,
|
|
93
|
+
locator: LocatorSpec,
|
|
94
|
+
payload: dict[str, Any] | None = None,
|
|
95
|
+
) -> dict[str, Any]:
|
|
96
|
+
return await self._page_command(
|
|
97
|
+
page_id,
|
|
98
|
+
action,
|
|
99
|
+
{"locator": locator.to_dict(), **(payload or {})},
|
|
100
|
+
)
|