implement same functionality into BrowserCLI python package
This commit is contained in:
+48
-8
@@ -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,6 +54,9 @@ 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 []
|
||||||
|
if self._remote:
|
||||||
|
targets = remote_browser_targets(self._remote, self._token)
|
||||||
|
else:
|
||||||
targets = active_browser_targets()
|
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 []
|
||||||
@@ -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 [])
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user