implement same functionality into BrowserCLI python package

This commit is contained in:
2026-05-01 20:01:55 +02:00
parent 647867d05e
commit ffa76f424a
2 changed files with 99 additions and 9 deletions
+49 -9
View File
@@ -19,7 +19,7 @@ Usage:
from collections.abc import Callable, Iterable from collections.abc import Callable, Iterable
from dataclasses import dataclass from dataclasses import dataclass
from browser_cli.client import BrowserNotConnected, active_browser_targets, send_command from browser_cli.client import BrowserNotConnected, active_browser_targets, remote_browser_targets, send_command
from browser_cli.models import Group, Tab from browser_cli.models import Group, Tab
__all__ = ["BrowserCLI", "BrowserCounts", "BrowserNotConnected", "Tab", "Group"] __all__ = ["BrowserCLI", "BrowserCounts", "BrowserNotConnected", "Tab", "Group"]
@@ -40,7 +40,8 @@ class BrowserCLI:
instances are active. Equivalent to ``--browser`` on the CLI. instances are active. Equivalent to ``--browser`` on the CLI.
remote: Connect to a remote browser exposed via ``browser-cli serve``. remote: Connect to a remote browser exposed via ``browser-cli serve``.
Format: ``"host:port"`` (e.g. ``"192.168.1.10:8765"``). Format: ``"host:port"`` (e.g. ``"192.168.1.10:8765"``).
When set, ``browser`` is ignored. Can be combined with ``browser`` to route to a specific
remote profile.
token: Auth token for the remote serve instance. token: Auth token for the remote serve instance.
""" """
self._browser = browser self._browser = browser
@@ -53,7 +54,10 @@ class BrowserCLI:
def _multi_browser_targets(self): def _multi_browser_targets(self):
if self._browser is not None: if self._browser is not None:
return [] return []
targets = active_browser_targets() if self._remote:
targets = remote_browser_targets(self._remote, self._token)
else:
targets = active_browser_targets()
if len(targets) <= 1 and not any(target.remote for target in targets): if len(targets) <= 1 and not any(target.remote for target in targets):
return [] return []
return targets return targets
@@ -81,7 +85,15 @@ class BrowserCLI:
# ── Internal factories ──────────────────────────────────────────────── # ── Internal factories ────────────────────────────────────────────────
def _make_tab(self, data: dict, *, browser_profile: str | None = None, browser_name: str | None = None) -> Tab: def _make_tab(
self,
data: dict,
*,
browser_profile: str | None = None,
browser_name: str | None = None,
browser_remote: str | None = None,
browser_token: str | None = None,
) -> Tab:
tab = Tab( tab = Tab(
id=data["id"], id=data["id"],
window_id=data.get("windowId", 0), window_id=data.get("windowId", 0),
@@ -92,10 +104,22 @@ class BrowserCLI:
group_id=data.get("groupId") or None, group_id=data.get("groupId") or None,
browser=browser_name, browser=browser_name,
) )
tab._browser = self if browser_profile is None else BrowserCLI(browser=browser_profile) tab._browser = self if browser_profile is None else BrowserCLI(
browser=browser_profile,
remote=browser_remote,
token=browser_token,
)
return tab return tab
def _make_group(self, data: dict, *, browser_profile: str | None = None, browser_name: str | None = None) -> Group: def _make_group(
self,
data: dict,
*,
browser_profile: str | None = None,
browser_name: str | None = None,
browser_remote: str | None = None,
browser_token: str | None = None,
) -> Group:
group = Group( group = Group(
id=data["id"], id=data["id"],
title=data.get("title") or "", title=data.get("title") or "",
@@ -104,7 +128,11 @@ class BrowserCLI:
tab_count=data.get("tabCount", 0), tab_count=data.get("tabCount", 0),
browser=browser_name, browser=browser_name,
) )
group._browser = self if browser_profile is None else BrowserCLI(browser=browser_profile) group._browser = self if browser_profile is None else BrowserCLI(
browser=browser_profile,
remote=browser_remote,
token=browser_token,
)
return group return group
# ── Navigation ──────────────────────────────────────────────────────── # ── Navigation ────────────────────────────────────────────────────────
@@ -196,7 +224,13 @@ class BrowserCLI:
multi_results = self._collect_multi_browser("tabs.list", {}) multi_results = self._collect_multi_browser("tabs.list", {})
if multi_results: if multi_results:
return [ return [
self._make_tab(tab, browser_profile=target.profile, browser_name=target.display_name) self._make_tab(
tab,
browser_profile=target.profile,
browser_name=target.display_name,
browser_remote=target.remote,
browser_token=target.token,
)
for target, tabs in multi_results for target, tabs in multi_results
for tab in (tabs or []) for tab in (tabs or [])
] ]
@@ -343,7 +377,13 @@ class BrowserCLI:
multi_results = self._collect_multi_browser("group.list", {}) multi_results = self._collect_multi_browser("group.list", {})
if multi_results: if multi_results:
return [ return [
self._make_group(group, browser_profile=target.profile, browser_name=target.display_name) self._make_group(
group,
browser_profile=target.profile,
browser_name=target.display_name,
browser_remote=target.remote,
browser_token=target.token,
)
for target, groups in multi_results for target, groups in multi_results
for group in (groups or []) for group in (groups or [])
] ]
+50
View File
@@ -5,6 +5,7 @@ These tests mock `send_command` so no live browser connection is required.
import pytest import pytest
from unittest.mock import MagicMock, patch, call from unittest.mock import MagicMock, patch, call
import browser_cli
from browser_cli import BrowserCLI, BrowserCounts, Tab, Group from browser_cli import BrowserCLI, BrowserCounts, Tab, Group
from browser_cli.client import BrowserNotConnected, BrowserTarget from browser_cli.client import BrowserNotConnected, BrowserTarget
@@ -63,6 +64,12 @@ class TestBrowserCLIInit:
b = BrowserCLI(browser="chrome") b = BrowserCLI(browser="chrome")
assert b._browser == "chrome" assert b._browser == "chrome"
def test_remote_options_stored(self):
b = BrowserCLI(browser="work", remote="host:8765", token="secret")
assert b._browser == "work"
assert b._remote == "host:8765"
assert b._token == "secret"
# ── Internal factories ──────────────────────────────────────────────────────── # ── Internal factories ────────────────────────────────────────────────────────
@@ -164,6 +171,11 @@ class TestNavigation:
b_profile.reload() b_profile.reload()
mock_send.assert_called_once_with("navigate.reload", {"tabId": None}, profile="brave", remote=None, token=None) mock_send.assert_called_once_with("navigate.reload", {"tabId": None}, profile="brave", remote=None, token=None)
def test_remote_forwarded(self, mock_send):
b = BrowserCLI(browser="work", remote="host:8765", token="secret")
b.reload()
mock_send.assert_called_once_with("navigate.reload", {"tabId": None}, profile="work", remote="host:8765", token="secret")
# ── Search ──────────────────────────────────────────────────────────────────── # ── Search ────────────────────────────────────────────────────────────────────
@@ -311,6 +323,25 @@ class TestTabs:
call("tabs.close", {"tabId": 11}, profile="work", remote=None, token=None), call("tabs.close", {"tabId": 11}, profile="work", remote=None, token=None),
] ]
def test_tabs_list_remote_uses_only_requested_remote_and_binds_actions(self, mock_send):
b = BrowserCLI(remote="host:8765", token="secret")
with patch(
"browser_cli.active_browser_targets",
side_effect=AssertionError("local targets should not be used for explicit remote"),
), patch(
"browser_cli.remote_browser_targets",
return_value=[BrowserTarget("work", "host:work", "", remote="host:8765", token="secret")],
):
mock_send.side_effect = [[TAB_DATA], None]
tabs = b.tabs_list()
tabs[0].close()
assert [tab.browser for tab in tabs] == ["host:work"]
assert mock_send.call_args_list == [
call("tabs.list", {}, profile="work", remote="host:8765", token="secret"),
call("tabs.close", {"tabId": 10}, profile="work", remote="host:8765", token="secret"),
]
def test_tabs_count_multi_browser_returns_browser_counts(self, b, mock_send): def test_tabs_count_multi_browser_returns_browser_counts(self, b, mock_send):
with patch( with patch(
"browser_cli.active_browser_targets", "browser_cli.active_browser_targets",
@@ -415,6 +446,25 @@ class TestGroups:
call("group.close", {"groupId": 99}, profile="work", remote=None, token=None), call("group.close", {"groupId": 99}, profile="work", remote=None, token=None),
] ]
def test_group_list_remote_uses_only_requested_remote_and_binds_actions(self, mock_send):
b = BrowserCLI(remote="host:8765", token="secret")
with patch(
"browser_cli.active_browser_targets",
side_effect=AssertionError("local targets should not be used for explicit remote"),
), patch(
"browser_cli.remote_browser_targets",
return_value=[BrowserTarget("work", "host:work", "", remote="host:8765", token="secret")],
):
mock_send.side_effect = [[GROUP_DATA], None]
groups = b.group_list()
groups[0].close()
assert [group.browser for group in groups] == ["host:work"]
assert mock_send.call_args_list == [
call("group.list", {}, profile="work", remote="host:8765", token="secret"),
call("group.close", {"groupId": 42}, profile="work", remote="host:8765", token="secret"),
]
def test_group_count_multi_browser_returns_browser_counts(self, b, mock_send): def test_group_count_multi_browser_returns_browser_counts(self, b, mock_send):
with patch( with patch(
"browser_cli.active_browser_targets", "browser_cli.active_browser_targets",