refactor(api): namespaced SDK + dedicated transport layer
Testing / remote-protocol-compat (0.9.3) (push) Successful in 42s
Testing / remote-protocol-compat (0.9.5) (push) Successful in 44s
Package Extension / package-extension (push) Successful in 43s
Build & Publish Package / publish (push) Successful in 43s
Testing / test (push) Successful in 45s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 42s
Testing / remote-protocol-compat (0.9.5) (push) Successful in 44s
Package Extension / package-extension (push) Successful in 43s
Build & Publish Package / publish (push) Successful in 43s
Testing / test (push) Successful in 45s
Restructure the Python API and internals around composable namespaces and a standalone transport/endpoint layer. Bump to 0.12.0. Python API: - Replace flat methods (b.tabs_list(), b.group_list()) with namespaces: b.nav, b.tabs, b.groups, b.windows, b.dom, b.extract, b.page, b.storage, b.cookies, b.session, b.perf, b.extension. - Shrink browser_cli/__init__.py to a thin composition root; move all behaviour into browser_cli/sdk/ (one module per namespace + factories, base, routing). Internals: - Add browser_cli/transport.py and remote_transport.py to isolate IPC from command logic; client.py now delegates instead of owning transport. - Add browser_cli/endpoints.py for endpoint resolution and browser_cli/errors.py for shared error types. - Extract markdown rendering into browser_cli/markdown.py (out of extract). - Add USER_AGENT to version_manager. Tooling & tests: - Add justfile with common dev tasks. - Update CLI commands and demo to the namespaced API. - Rework tests for the new layout; add test_transport.py and test_refactor_boundaries.py to lock in module boundaries. BREAKING CHANGE: flat API methods are removed in favour of namespaces (e.g. b.tabs_list() -> b.tabs.list(), b.group_list() -> b.groups.list()).
This commit is contained in:
+221
-135
@@ -1,6 +1,7 @@
|
||||
"""Unit tests for the BrowserCLI Python API (BrowserCLI, Tab, Group).
|
||||
"""Unit tests for the BrowserCLI Python SDK (BrowserCLI, namespaces, Tab, Group).
|
||||
|
||||
These tests mock `send_command` so no live browser connection is required.
|
||||
Commands live on namespace accessors: b.tabs.list(), b.dom.click(), b.groups.create(), ...
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch, call
|
||||
@@ -29,7 +30,6 @@ GROUP_DATA = {
|
||||
"tabCount": 3,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def mock_send():
|
||||
"""Patch send_command for the duration of one test.
|
||||
@@ -40,19 +40,16 @@ def mock_send():
|
||||
with patch("browser_cli.send_command") as m, patch("browser_cli.active_browser_targets", return_value=[]):
|
||||
yield m
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def b(mock_send):
|
||||
"""A BrowserCLI instance with send_command mocked."""
|
||||
return BrowserCLI()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def b_profile(mock_send):
|
||||
"""A BrowserCLI instance targeting a specific profile."""
|
||||
return BrowserCLI(browser="brave")
|
||||
|
||||
|
||||
# ── BrowserCLI construction ───────────────────────────────────────────────────
|
||||
|
||||
class TestBrowserCLIInit:
|
||||
@@ -75,6 +72,14 @@ class TestBrowserCLIInit:
|
||||
assert b.remote == "browser-host.example:443"
|
||||
assert b.key == "agent"
|
||||
|
||||
def test_namespaces_present_and_bound(self):
|
||||
b = BrowserCLI()
|
||||
for name in ("nav", "tabs", "groups", "windows", "dom", "extract",
|
||||
"page", "storage", "cookies", "session", "perf", "extension"):
|
||||
ns = getattr(b, name)
|
||||
assert ns is not None
|
||||
assert ns._c is b
|
||||
|
||||
def test_raw_command_escape_hatch(self, mock_send):
|
||||
mock_send.return_value = {"ok": True}
|
||||
b = BrowserCLI(browser="work", remote="browser-host.example:443", key="agent")
|
||||
@@ -94,7 +99,6 @@ class TestBrowserCLIInit:
|
||||
b.command("custom.command")
|
||||
mock_send.assert_called_once_with("custom.command", {}, profile=None, remote=None, key=None)
|
||||
|
||||
|
||||
# ── Internal factories ────────────────────────────────────────────────────────
|
||||
|
||||
class TestMakeTab:
|
||||
@@ -144,12 +148,11 @@ class TestMakeGroup:
|
||||
assert group.collapsed is False
|
||||
assert group.tab_count == 0
|
||||
|
||||
|
||||
# ── Navigation ────────────────────────────────────────────────────────────────
|
||||
|
||||
class TestNavigation:
|
||||
def test_open(self, b, mock_send):
|
||||
b.open("https://example.com")
|
||||
b.nav.open("https://example.com")
|
||||
mock_send.assert_called_once_with(
|
||||
"navigate.open",
|
||||
{"url": "https://example.com", "background": False, "window": None, "group": None},
|
||||
@@ -157,18 +160,18 @@ class TestNavigation:
|
||||
)
|
||||
|
||||
def test_open_background(self, b, mock_send):
|
||||
b.open("https://example.com", background=True)
|
||||
b.nav.open("https://example.com", background=True)
|
||||
args = mock_send.call_args[0]
|
||||
assert args[1]["background"] is True
|
||||
|
||||
def test_open_with_group(self, b, mock_send):
|
||||
b.open("https://x.com", group="Work")
|
||||
b.nav.open("https://x.com", group="Work")
|
||||
assert mock_send.call_args[0][1]["group"] == "Work"
|
||||
|
||||
def test_open_tab_returns_bound_tab(self, b, mock_send):
|
||||
def test_tabs_open_returns_bound_tab(self, b, mock_send):
|
||||
mock_send.return_value = {"id": 123, "url": "https://example.com"}
|
||||
|
||||
tab = b.open_tab("https://example.com", background=True)
|
||||
tab = b.tabs.open("https://example.com", background=True)
|
||||
|
||||
assert tab.id == 123
|
||||
assert tab.url == "https://example.com"
|
||||
@@ -181,10 +184,10 @@ class TestNavigation:
|
||||
key=None,
|
||||
)
|
||||
|
||||
def test_open_tab_wait_uses_open_wait(self, b, mock_send):
|
||||
def test_tabs_open_wait_uses_open_wait(self, b, mock_send):
|
||||
mock_send.return_value = TAB_DATA
|
||||
|
||||
tab = b.open_tab("https://example.com", wait=True, timeout=1.5)
|
||||
tab = b.tabs.open("https://example.com", wait=True, timeout=1.5)
|
||||
|
||||
assert tab.id == 10
|
||||
mock_send.assert_called_once_with(
|
||||
@@ -195,185 +198,199 @@ class TestNavigation:
|
||||
key=None,
|
||||
)
|
||||
|
||||
def test_open_tab_unexpected_response_raises(self, b, mock_send):
|
||||
def test_tabs_open_unexpected_response_raises(self, b, mock_send):
|
||||
mock_send.return_value = None
|
||||
with pytest.raises(RuntimeError, match="navigate.open returned unexpected data"):
|
||||
b.open_tab("https://example.com")
|
||||
b.tabs.open("https://example.com")
|
||||
|
||||
def test_nav_open_wait_unexpected_response_raises(self, b, mock_send):
|
||||
mock_send.return_value = None
|
||||
with pytest.raises(RuntimeError, match="navigate.open_wait returned unexpected data"):
|
||||
b.nav.open_wait("https://example.com")
|
||||
|
||||
def test_reload(self, b, mock_send):
|
||||
b.reload(tab_id=5)
|
||||
b.nav.reload(tab_id=5)
|
||||
mock_send.assert_called_once_with("navigate.reload", {"tabId": 5}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_hard_reload(self, b, mock_send):
|
||||
b.hard_reload(tab_id=7)
|
||||
b.nav.hard_reload(tab_id=7)
|
||||
mock_send.assert_called_once_with("navigate.hard_reload", {"tabId": 7}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_back(self, b, mock_send):
|
||||
b.back(tab_id=3)
|
||||
b.nav.back(tab_id=3)
|
||||
mock_send.assert_called_once_with("navigate.back", {"tabId": 3}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_forward(self, b, mock_send):
|
||||
b.forward(tab_id=3)
|
||||
b.nav.forward(tab_id=3)
|
||||
mock_send.assert_called_once_with("navigate.forward", {"tabId": 3}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_focus_url(self, b, mock_send):
|
||||
b.focus_url("github.com")
|
||||
def test_focus(self, b, mock_send):
|
||||
b.nav.focus("github.com")
|
||||
mock_send.assert_called_once_with("navigate.focus", {"pattern": "github.com"}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_navigate_tab(self, b, mock_send):
|
||||
b.navigate_tab(5, "https://example.com")
|
||||
def test_nav_to(self, b, mock_send):
|
||||
b.nav.to(5, "https://example.com")
|
||||
mock_send.assert_called_once_with(
|
||||
"navigate.to", {"tabId": 5, "url": "https://example.com"}, profile=None, remote=None, key=None
|
||||
)
|
||||
|
||||
def test_profile_forwarded(self, b_profile, mock_send):
|
||||
b_profile.reload()
|
||||
b_profile.nav.reload()
|
||||
mock_send.assert_called_once_with("navigate.reload", {"tabId": None}, profile="brave", remote=None, key=None)
|
||||
|
||||
def test_remote_forwarded(self, mock_send):
|
||||
b = BrowserCLI(browser="work", remote="host:8765", key=None)
|
||||
b.reload()
|
||||
b.nav.reload()
|
||||
mock_send.assert_called_once_with("navigate.reload", {"tabId": None}, profile="work", remote="host:8765", key=None)
|
||||
|
||||
|
||||
# ── Search ────────────────────────────────────────────────────────────────────
|
||||
|
||||
class TestSearch:
|
||||
def test_known_engine_builds_url(self, b, mock_send):
|
||||
b.search("google", "python asyncio")
|
||||
b.nav.search("google", "python asyncio")
|
||||
call_args = mock_send.call_args[0]
|
||||
assert call_args[0] == "navigate.open"
|
||||
assert "python+asyncio" in call_args[1]["url"] or "python%20asyncio" in call_args[1]["url"] or "python+asyncio" in call_args[1]["url"]
|
||||
assert "python+asyncio" in call_args[1]["url"] or "python%20asyncio" in call_args[1]["url"]
|
||||
|
||||
def test_unknown_engine_raises(self, b):
|
||||
with pytest.raises(ValueError, match="Unknown search engine"):
|
||||
b.search("nonexistent", "query")
|
||||
b.nav.search("nonexistent", "query")
|
||||
|
||||
def test_search_alias_ddg(self, b, mock_send):
|
||||
b.search("ddg", "hello")
|
||||
b.nav.search("ddg", "hello")
|
||||
url = mock_send.call_args[0][1]["url"]
|
||||
assert "duckduckgo.com" in url
|
||||
|
||||
def test_search_background(self, b, mock_send):
|
||||
b.search("bing", "test", background=True)
|
||||
b.nav.search("bing", "test", background=True)
|
||||
assert mock_send.call_args[0][1]["background"] is True
|
||||
|
||||
# ── Extract ───────────────────────────────────────────────────────────────────
|
||||
|
||||
class TestExtract:
|
||||
def test_extract_markdown_default(self, b, mock_send):
|
||||
mock_send.return_value = "# Title"
|
||||
|
||||
result = b.extract_markdown()
|
||||
result = b.extract.markdown()
|
||||
|
||||
assert result == "# Title"
|
||||
mock_send.assert_called_once_with("extract.markdown", {"selector": None}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_extract_markdown_selector(self, b, mock_send):
|
||||
b.extract_markdown("article")
|
||||
mock_send.return_value = "## Post"
|
||||
|
||||
assert b.extract.markdown("article") == "## Post"
|
||||
mock_send.assert_called_once_with("extract.markdown", {"selector": "article"}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_extract_links(self, b, mock_send):
|
||||
mock_send.return_value = [{"href": "https://x"}]
|
||||
assert b.extract.links() == [{"href": "https://x"}]
|
||||
mock_send.assert_called_once_with("extract.links", {}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_extract_text_none(self, b, mock_send):
|
||||
mock_send.return_value = None
|
||||
assert b.extract.text() == ""
|
||||
|
||||
# ── Tabs ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
class TestTabs:
|
||||
def test_tabs_list_returns_tab_objects(self, b, mock_send):
|
||||
mock_send.return_value = [TAB_DATA]
|
||||
tabs = b.tabs_list()
|
||||
tabs = b.tabs.list()
|
||||
assert len(tabs) == 1
|
||||
assert isinstance(tabs[0], Tab)
|
||||
assert tabs[0].id == 10
|
||||
|
||||
def test_tabs_list_empty(self, b, mock_send):
|
||||
mock_send.return_value = []
|
||||
assert b.tabs_list() == []
|
||||
assert b.tabs.list() == []
|
||||
|
||||
def test_tabs_list_none_response(self, b, mock_send):
|
||||
mock_send.return_value = None
|
||||
assert b.tabs_list() == []
|
||||
assert b.tabs.list() == []
|
||||
|
||||
def test_tabs_close_returns_count(self, b, mock_send):
|
||||
mock_send.return_value = {"closed": 3}
|
||||
assert b.tabs_close() == 3
|
||||
assert b.tabs.close() == 3
|
||||
|
||||
def test_tabs_close_non_dict_response(self, b, mock_send):
|
||||
mock_send.return_value = None
|
||||
assert b.tabs_close() == 1
|
||||
assert b.tabs.close() == 1
|
||||
|
||||
def test_tabs_close_by_id(self, b, mock_send):
|
||||
mock_send.return_value = {"closed": 1}
|
||||
b.tabs_close(tab_id=10)
|
||||
b.tabs.close(tab_id=10)
|
||||
mock_send.assert_called_once_with(
|
||||
"tabs.close",
|
||||
{"tabId": 10, "tabIds": None, "inactive": False, "duplicates": False},
|
||||
{"tabId": 10, "tabIds": None, "inactive": False, "duplicates": False, "gentleMode": "auto"},
|
||||
profile=None, remote=None, key=None,
|
||||
)
|
||||
|
||||
def test_tabs_close_by_ids(self, b, mock_send):
|
||||
mock_send.return_value = {"closed": 3}
|
||||
assert b.tabs_close(tab_ids=[10, 20, 30]) == 3
|
||||
assert b.tabs.close(tab_ids=[10, 20, 30]) == 3
|
||||
mock_send.assert_called_once_with(
|
||||
"tabs.close",
|
||||
{"tabId": None, "tabIds": [10, 20, 30], "inactive": False, "duplicates": False},
|
||||
{"tabId": None, "tabIds": [10, 20, 30], "inactive": False, "duplicates": False, "gentleMode": "auto"},
|
||||
profile=None, remote=None, key=None,
|
||||
)
|
||||
|
||||
def test_tabs_close_by_ids_accepts_tab_objects(self, b, mock_send):
|
||||
tabs = [b._make_tab({**TAB_DATA, "id": 10}), b._make_tab({**TAB_DATA, "id": 20})]
|
||||
mock_send.return_value = {"closed": 2}
|
||||
assert b.tabs_close(tab_ids=tabs) == 2
|
||||
assert b.tabs.close(tab_ids=tabs) == 2
|
||||
mock_send.assert_called_once_with(
|
||||
"tabs.close",
|
||||
{"tabId": None, "tabIds": [10, 20], "inactive": False, "duplicates": False},
|
||||
{"tabId": None, "tabIds": [10, 20], "inactive": False, "duplicates": False, "gentleMode": "auto"},
|
||||
profile=None, remote=None, key=None,
|
||||
)
|
||||
|
||||
def test_tabs_move(self, b, mock_send):
|
||||
b.tabs_move(10, forward=True)
|
||||
b.tabs.move(10, forward=True)
|
||||
mock_send.assert_called_once_with(
|
||||
"tabs.move",
|
||||
{"tabId": 10, "forward": True, "backward": False, "groupId": None, "windowId": None, "index": None},
|
||||
profile=None, remote=None, key=None,
|
||||
)
|
||||
|
||||
def test_tabs_active(self, b, mock_send):
|
||||
b.tabs_active(10)
|
||||
def test_tabs_activate(self, b, mock_send):
|
||||
b.tabs.activate(10)
|
||||
mock_send.assert_called_once_with("tabs.active", {"tabId": 10}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_window_active_tab(self, b, mock_send):
|
||||
def test_tabs_active_in_window(self, b, mock_send):
|
||||
mock_send.return_value = TAB_DATA
|
||||
tab = b.window_active_tab(1)
|
||||
tab = b.tabs.active_in_window(1)
|
||||
assert isinstance(tab, Tab)
|
||||
assert tab.id == 10
|
||||
mock_send.assert_called_once_with("tabs.active_in_window", {"windowId": 1}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_window_active_tab_missing_raises(self, b, mock_send):
|
||||
def test_tabs_active_in_window_missing_raises(self, b, mock_send):
|
||||
mock_send.return_value = None
|
||||
with pytest.raises(RuntimeError, match="No active tab found for window 1"):
|
||||
b.window_active_tab(1)
|
||||
b.tabs.active_in_window(1)
|
||||
|
||||
def test_tabs_filter(self, b, mock_send):
|
||||
mock_send.return_value = [TAB_DATA]
|
||||
tabs = b.tabs_filter("example")
|
||||
tabs = b.tabs.filter("example")
|
||||
assert isinstance(tabs[0], Tab)
|
||||
|
||||
def test_tabs_filter_none(self, b, mock_send):
|
||||
mock_send.return_value = None
|
||||
assert b.tabs_filter("x") == []
|
||||
assert b.tabs.filter("x") == []
|
||||
|
||||
def test_tabs_filter_predicate(self, b, mock_send):
|
||||
mock_send.return_value = [TAB_DATA, {**TAB_DATA, "id": 11, "url": "https://youtube.com"}]
|
||||
tabs = b.tabs_filter(lambda tab: "youtube" in tab.url)
|
||||
tabs = b.tabs.filter(lambda tab: "youtube" in tab.url)
|
||||
assert [tab.id for tab in tabs] == [11]
|
||||
|
||||
def test_tabs_filter_list_transformer(self, b, mock_send):
|
||||
mock_send.return_value = [TAB_DATA, {**TAB_DATA, "id": 11, "url": "https://example.com"}]
|
||||
tabs = b.tabs_filter(lambda tabs: tabs[:1])
|
||||
tabs = b.tabs.filter(lambda tabs: tabs[:1])
|
||||
assert [tab.id for tab in tabs] == [10]
|
||||
|
||||
def test_tabs_count(self, b, mock_send):
|
||||
mock_send.return_value = 5
|
||||
assert b.tabs_count() == 5
|
||||
assert b.tabs.count() == 5
|
||||
|
||||
def test_tabs_list_multi_browser_annotates_browser_and_binds_actions(self, b, mock_send):
|
||||
with patch(
|
||||
@@ -389,7 +406,7 @@ class TestTabs:
|
||||
None,
|
||||
]
|
||||
|
||||
tabs = b.tabs_list()
|
||||
tabs = b.tabs.list()
|
||||
tabs[1].close()
|
||||
|
||||
assert [tab.browser for tab in tabs] == ["uuid-1", "work"]
|
||||
@@ -410,7 +427,7 @@ class TestTabs:
|
||||
return_value=[BrowserTarget("work", "host:work", "", remote="host:8765")],
|
||||
):
|
||||
mock_send.side_effect = [[TAB_DATA], None]
|
||||
tabs = b.tabs_list()
|
||||
tabs = b.tabs.list()
|
||||
tabs[0].close()
|
||||
|
||||
assert [tab.browser for tab in tabs] == ["host:work"]
|
||||
@@ -426,7 +443,7 @@ class TestTabs:
|
||||
return_value=[BrowserTarget("work", "browser-host.example:work", "", remote="browser-host.example")],
|
||||
):
|
||||
mock_send.side_effect = [[TAB_DATA], None]
|
||||
tabs = b.tabs_list()
|
||||
tabs = b.tabs.list()
|
||||
tabs[0].close()
|
||||
|
||||
assert mock_send.call_args_list == [
|
||||
@@ -434,11 +451,11 @@ class TestTabs:
|
||||
call("tabs.close", {"tabId": 10}, profile="work", remote="browser-host.example", key="agent"),
|
||||
]
|
||||
|
||||
def test_tabs_alias_and_active_tab(self, b, mock_send):
|
||||
def test_tabs_active_returns_active_tab(self, b, mock_send):
|
||||
mock_send.side_effect = [[TAB_DATA], TAB_DATA]
|
||||
|
||||
tabs = b.tabs()
|
||||
active = b.active_tab()
|
||||
tabs = b.tabs.list()
|
||||
active = b.tabs.active()
|
||||
|
||||
assert tabs[0].id == 10
|
||||
assert active.id == 10
|
||||
@@ -447,14 +464,14 @@ class TestTabs:
|
||||
call("tabs.status", {"tabId": None}, profile=None, remote=None, key=None),
|
||||
]
|
||||
|
||||
def test_tab_find_and_close_helpers(self, b, mock_send):
|
||||
def test_tabs_get_first_and_close_helpers(self, b, mock_send):
|
||||
mock_send.side_effect = [TAB_DATA, [TAB_DATA], [], {"closed": 1}, {"closed": 1}]
|
||||
|
||||
tab = b.tab(10)
|
||||
found = b.find_tab("Example")
|
||||
missing = b.find_tab("Missing")
|
||||
closed_by_object = b.close_tab(tab)
|
||||
closed_by_id = b.close_tab(10)
|
||||
tab = b.tabs.get(10)
|
||||
found = b.tabs.first("Example")
|
||||
missing = b.tabs.first("Missing")
|
||||
closed_by_object = b.tabs.close(tab.id)
|
||||
closed_by_id = b.tabs.close(10)
|
||||
|
||||
assert tab.id == 10
|
||||
assert found and found.id == 10
|
||||
@@ -465,18 +482,10 @@ class TestTabs:
|
||||
call("tabs.status", {"tabId": 10}, profile=None, remote=None, key=None),
|
||||
call("tabs.query", {"search": "Example"}, profile=None, remote=None, key=None),
|
||||
call("tabs.query", {"search": "Missing"}, profile=None, remote=None, key=None),
|
||||
call("tabs.close", {"tabId": 10, "tabIds": None, "inactive": False, "duplicates": False}, profile=None, remote=None, key=None),
|
||||
call("tabs.close", {"tabId": 10, "tabIds": None, "inactive": False, "duplicates": False}, profile=None, remote=None, key=None),
|
||||
call("tabs.close", {"tabId": 10, "tabIds": None, "inactive": False, "duplicates": False, "gentleMode": "auto"}, profile=None, remote=None, key=None),
|
||||
call("tabs.close", {"tabId": 10, "tabIds": None, "inactive": False, "duplicates": False, "gentleMode": "auto"}, profile=None, remote=None, key=None),
|
||||
]
|
||||
|
||||
def test_find_tabs_alias(self, b, mock_send):
|
||||
mock_send.return_value = [TAB_DATA]
|
||||
|
||||
tabs = b.find_tabs("Example")
|
||||
|
||||
assert [tab.id for tab in tabs] == [10]
|
||||
mock_send.assert_called_once_with("tabs.query", {"search": "Example"}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_tabs_count_multi_browser_returns_browser_counts(self, b, mock_send):
|
||||
with patch(
|
||||
"browser_cli.active_browser_targets",
|
||||
@@ -486,7 +495,7 @@ class TestTabs:
|
||||
],
|
||||
):
|
||||
mock_send.side_effect = [3, 4]
|
||||
result = b.tabs_count("github")
|
||||
result = b.tabs.count("github")
|
||||
|
||||
assert result == BrowserCounts(total=7, by_browser={"uuid-1": 3, "work": 4})
|
||||
assert mock_send.call_args_list == [
|
||||
@@ -496,65 +505,81 @@ class TestTabs:
|
||||
|
||||
def test_tabs_query(self, b, mock_send):
|
||||
mock_send.return_value = [TAB_DATA]
|
||||
result = b.tabs_query("example")
|
||||
result = b.tabs.query("example")
|
||||
assert isinstance(result[0], Tab)
|
||||
mock_send.assert_called_once_with("tabs.query", {"search": "example"}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_tabs_html(self, b, mock_send):
|
||||
mock_send.return_value = "<html></html>"
|
||||
assert b.tabs_html(10) == "<html></html>"
|
||||
assert b.tabs.html(10) == "<html></html>"
|
||||
|
||||
def test_tabs_html_none(self, b, mock_send):
|
||||
mock_send.return_value = None
|
||||
assert b.tabs_html() == ""
|
||||
assert b.tabs.html() == ""
|
||||
|
||||
def test_tabs_dedupe(self, b, mock_send):
|
||||
mock_send.return_value = {"closed": 2}
|
||||
assert b.tabs_dedupe() == 2
|
||||
assert b.tabs.dedupe() == 2
|
||||
|
||||
def test_tabs_dedupe_non_dict(self, b, mock_send):
|
||||
mock_send.return_value = None
|
||||
assert b.tabs_dedupe() == 0
|
||||
assert b.tabs.dedupe() == 0
|
||||
|
||||
def test_tabs_sort(self, b, mock_send):
|
||||
b.tabs_sort(by="title")
|
||||
mock_send.assert_called_once_with("tabs.sort", {"by": "title"}, profile=None, remote=None, key=None)
|
||||
b.tabs.sort(by="title")
|
||||
mock_send.assert_called_once_with("tabs.sort", {"by": "title", "gentleMode": "auto"}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_tabs_merge_windows(self, b, mock_send):
|
||||
mock_send.return_value = {"moved": 4}
|
||||
assert b.tabs_merge_windows() == 4
|
||||
assert b.tabs.merge_windows() == 4
|
||||
|
||||
def test_tabs_close_inactive(self, b, mock_send):
|
||||
mock_send.return_value = {"closed": 2}
|
||||
assert b.tabs_close_inactive() == 2
|
||||
assert b.tabs.close_inactive() == 2
|
||||
|
||||
def test_tabs_close_duplicates(self, b, mock_send):
|
||||
mock_send.return_value = {"closed": 1}
|
||||
assert b.tabs_close_duplicates() == 1
|
||||
assert b.tabs.close_duplicates() == 1
|
||||
|
||||
def test_tabs_mute_pin_return_tab_id(self, b, mock_send):
|
||||
mock_send.side_effect = [{"tabId": 10}, None]
|
||||
assert b.tabs.mute(10) == 10
|
||||
assert b.tabs.pin() == 0
|
||||
assert mock_send.call_args_list == [
|
||||
call("tabs.mute", {"tabId": 10}, profile=None, remote=None, key=None),
|
||||
call("tabs.pin", {"tabId": None}, profile=None, remote=None, key=None),
|
||||
]
|
||||
|
||||
def test_tabs_screenshot(self, b, mock_send):
|
||||
mock_send.return_value = {"dataUrl": "data:image/png;base64,abc"}
|
||||
assert b.tabs.screenshot(10) == "data:image/png;base64,abc"
|
||||
mock_send.assert_called_once_with(
|
||||
"tabs.screenshot", {"tabId": 10, "format": "png", "quality": None}, profile=None, remote=None, key=None
|
||||
)
|
||||
|
||||
# ── Groups ────────────────────────────────────────────────────────────────────
|
||||
|
||||
class TestGroups:
|
||||
def test_group_list_returns_group_objects(self, b, mock_send):
|
||||
mock_send.return_value = [GROUP_DATA]
|
||||
groups = b.group_list()
|
||||
groups = b.groups.list()
|
||||
assert len(groups) == 1
|
||||
assert isinstance(groups[0], Group)
|
||||
assert groups[0].id == 42
|
||||
|
||||
def test_group_list_empty(self, b, mock_send):
|
||||
mock_send.return_value = None
|
||||
assert b.group_list() == []
|
||||
assert b.groups.list() == []
|
||||
|
||||
def test_group_tabs(self, b, mock_send):
|
||||
mock_send.return_value = [TAB_DATA]
|
||||
tabs = b.group_tabs(42)
|
||||
tabs = b.groups.tabs(42)
|
||||
assert isinstance(tabs[0], Tab)
|
||||
mock_send.assert_called_once_with("group.tabs", {"groupId": 42}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_group_count(self, b, mock_send):
|
||||
mock_send.return_value = 7
|
||||
assert b.group_count() == 7
|
||||
assert b.groups.count() == 7
|
||||
|
||||
def test_group_list_multi_browser_annotates_browser_and_binds_actions(self, b, mock_send):
|
||||
with patch(
|
||||
@@ -570,7 +595,7 @@ class TestGroups:
|
||||
None,
|
||||
]
|
||||
|
||||
groups = b.group_list()
|
||||
groups = b.groups.list()
|
||||
groups[1].close()
|
||||
|
||||
assert [group.browser for group in groups] == ["uuid-1", "work"]
|
||||
@@ -591,7 +616,7 @@ class TestGroups:
|
||||
return_value=[BrowserTarget("work", "host:work", "", remote="host:8765")],
|
||||
):
|
||||
mock_send.side_effect = [[GROUP_DATA], None]
|
||||
groups = b.group_list()
|
||||
groups = b.groups.list()
|
||||
groups[0].close()
|
||||
|
||||
assert [group.browser for group in groups] == ["host:work"]
|
||||
@@ -607,7 +632,7 @@ class TestGroups:
|
||||
return_value=[BrowserTarget("work", "browser-host.example:work", "", remote="browser-host.example")],
|
||||
):
|
||||
mock_send.side_effect = [[GROUP_DATA], None]
|
||||
groups = b.group_list()
|
||||
groups = b.groups.list()
|
||||
groups[0].close()
|
||||
|
||||
assert mock_send.call_args_list == [
|
||||
@@ -615,25 +640,6 @@ class TestGroups:
|
||||
call("group.close", {"groupId": 42}, profile="work", remote="browser-host.example", key="agent"),
|
||||
]
|
||||
|
||||
def test_group_aliases(self, b, mock_send):
|
||||
mock_send.side_effect = [[GROUP_DATA], [GROUP_DATA], 1, [GROUP_DATA], GROUP_DATA, GROUP_DATA]
|
||||
|
||||
assert b.groups()[0].id == 42
|
||||
assert b.groups_list()[0].id == 42
|
||||
assert b.groups_count() == 1
|
||||
assert b.groups_query("Work")[0].id == 42
|
||||
assert b.groups_create("Work").id == 42
|
||||
assert b.group_open("Work").id == 42
|
||||
|
||||
assert mock_send.call_args_list == [
|
||||
call("group.list", {}, profile=None, remote=None, key=None),
|
||||
call("group.list", {}, profile=None, remote=None, key=None),
|
||||
call("group.count", {}, profile=None, remote=None, key=None),
|
||||
call("group.query", {"search": "Work"}, profile=None, remote=None, key=None),
|
||||
call("group.open", {"name": "Work"}, profile=None, remote=None, key=None),
|
||||
call("group.open", {"name": "Work"}, profile=None, remote=None, key=None),
|
||||
]
|
||||
|
||||
def test_group_count_multi_browser_returns_browser_counts(self, b, mock_send):
|
||||
with patch(
|
||||
"browser_cli.active_browser_targets",
|
||||
@@ -643,35 +649,36 @@ class TestGroups:
|
||||
],
|
||||
):
|
||||
mock_send.side_effect = [2, 5]
|
||||
result = b.group_count()
|
||||
result = b.groups.count()
|
||||
|
||||
assert result == BrowserCounts(total=7, by_browser={"uuid-1": 2, "work": 5})
|
||||
|
||||
def test_group_query(self, b, mock_send):
|
||||
mock_send.return_value = [GROUP_DATA]
|
||||
groups = b.group_query("Work")
|
||||
groups = b.groups.query("Work")
|
||||
assert isinstance(groups[0], Group)
|
||||
mock_send.assert_called_once_with("group.query", {"search": "Work"}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_group_close(self, b, mock_send):
|
||||
b.group_close(42)
|
||||
mock_send.assert_called_once_with("group.close", {"groupId": 42}, profile=None, remote=None, key=None)
|
||||
b.groups.close(42)
|
||||
mock_send.assert_called_once_with("group.close", {"groupId": 42, "gentleMode": "auto"}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_group_create_dict_response(self, b, mock_send):
|
||||
mock_send.return_value = GROUP_DATA
|
||||
group = b.group_create("Work")
|
||||
group = b.groups.create("Work")
|
||||
assert isinstance(group, Group)
|
||||
assert group.id == 42
|
||||
assert group.title == "Work"
|
||||
|
||||
def test_group_create_int_response(self, b, mock_send):
|
||||
mock_send.return_value = 99
|
||||
group = b.group_create("Misc")
|
||||
group = b.groups.create("Misc")
|
||||
assert group.id == 99
|
||||
assert group.title == "Misc"
|
||||
|
||||
def test_group_add_tab(self, b, mock_send):
|
||||
mock_send.return_value = {"tabId": 55}
|
||||
tab_id = b.group_add_tab(42, "https://example.com")
|
||||
tab_id = b.groups.add_tab(42, "https://example.com")
|
||||
assert tab_id == 55
|
||||
mock_send.assert_called_once_with(
|
||||
"group.add_tab", {"group": "42", "url": "https://example.com"}, profile=None, remote=None, key=None
|
||||
@@ -679,15 +686,14 @@ class TestGroups:
|
||||
|
||||
def test_group_add_tab_non_dict_response(self, b, mock_send):
|
||||
mock_send.return_value = 55
|
||||
assert b.group_add_tab(42) == 55
|
||||
assert b.groups.add_tab(42) == 55
|
||||
|
||||
def test_group_move_forward(self, b, mock_send):
|
||||
b.group_move(42, forward=True)
|
||||
b.groups.move(42, forward=True)
|
||||
mock_send.assert_called_once_with(
|
||||
"group.move", {"group": "42", "forward": True, "backward": False}, profile=None, remote=None, key=None
|
||||
)
|
||||
|
||||
|
||||
class TestWindows:
|
||||
def test_windows_list_multi_browser_adds_browser(self, b, mock_send):
|
||||
with patch(
|
||||
@@ -701,7 +707,7 @@ class TestWindows:
|
||||
[{"id": 1, "tabCount": 2, "state": "normal"}],
|
||||
[{"id": 2, "tabCount": 3, "state": "maximized"}],
|
||||
]
|
||||
result = b.windows_list()
|
||||
result = b.windows.list()
|
||||
|
||||
assert result == [
|
||||
{"id": 1, "tabCount": 2, "state": "normal", "browser": "uuid-1"},
|
||||
@@ -711,7 +717,7 @@ class TestWindows:
|
||||
def test_windows_open_without_url(self, b, mock_send):
|
||||
mock_send.return_value = {"id": 5}
|
||||
|
||||
result = b.windows_open()
|
||||
result = b.windows.open()
|
||||
|
||||
assert result == {"id": 5}
|
||||
mock_send.assert_called_once_with("windows.open", {"url": None}, profile=None, remote=None, key=None)
|
||||
@@ -719,17 +725,32 @@ class TestWindows:
|
||||
def test_windows_open_with_url(self, b, mock_send):
|
||||
mock_send.return_value = {"id": 9}
|
||||
|
||||
result = b.windows_open("https://example.com")
|
||||
result = b.windows.open("https://example.com")
|
||||
|
||||
assert result == {"id": 9}
|
||||
mock_send.assert_called_once_with("windows.open", {"url": "https://example.com"}, profile=None, remote=None, key=None)
|
||||
|
||||
class TestDom:
|
||||
def test_dom_click(self, b, mock_send):
|
||||
b.dom.click("#go")
|
||||
mock_send.assert_called_once_with("dom.click", {"selector": "#go"}, profile=None, remote=None, key=None)
|
||||
|
||||
class TestDomConvenience:
|
||||
def test_wait_for_selector_alias(self, b, mock_send):
|
||||
def test_dom_type(self, b, mock_send):
|
||||
b.dom.type("#in", "hello")
|
||||
mock_send.assert_called_once_with("dom.type", {"selector": "#in", "text": "hello"}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_dom_query_none(self, b, mock_send):
|
||||
mock_send.return_value = None
|
||||
assert b.dom.query("a") == []
|
||||
|
||||
def test_dom_exists_falsy(self, b, mock_send):
|
||||
mock_send.return_value = None
|
||||
assert b.dom.exists("a") is False
|
||||
|
||||
def test_dom_wait_for(self, b, mock_send):
|
||||
mock_send.return_value = {"selector": "#done", "found": True}
|
||||
|
||||
result = b.wait_for_selector("#done", timeout=2.5, visible=True, tab_id=10)
|
||||
result = b.dom.wait_for("#done", timeout=2.5, visible=True, tab_id=10)
|
||||
|
||||
assert result == {"selector": "#done", "found": True}
|
||||
mock_send.assert_called_once_with(
|
||||
@@ -740,12 +761,78 @@ class TestDomConvenience:
|
||||
key=None,
|
||||
)
|
||||
|
||||
def test_dom_eval(self, b, mock_send):
|
||||
mock_send.return_value = 42
|
||||
assert b.dom.eval("1+41", tab_id=3) == 42
|
||||
mock_send.assert_called_once_with("dom.eval", {"code": "1+41", "tabId": 3}, profile=None, remote=None, key=None)
|
||||
|
||||
class TestPageStorageCookies:
|
||||
def test_page_info(self, b, mock_send):
|
||||
mock_send.return_value = {"title": "X"}
|
||||
assert b.page.info() == {"title": "X"}
|
||||
mock_send.assert_called_once_with("page.info", {}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_storage_get(self, b, mock_send):
|
||||
mock_send.return_value = "v"
|
||||
assert b.storage.get("k") == "v"
|
||||
mock_send.assert_called_once_with("storage.get", {"key": "k", "type": "local", "tabId": None}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_storage_set(self, b, mock_send):
|
||||
b.storage.set("k", "v", type="session")
|
||||
mock_send.assert_called_once_with(
|
||||
"storage.set", {"key": "k", "value": "v", "type": "session", "tabId": None}, profile=None, remote=None, key=None
|
||||
)
|
||||
|
||||
def test_cookies_list(self, b, mock_send):
|
||||
mock_send.return_value = [{"name": "c"}]
|
||||
assert b.cookies.list(domain="example.com") == [{"name": "c"}]
|
||||
mock_send.assert_called_once_with(
|
||||
"cookies.list", {"url": None, "domain": "example.com", "name": None}, profile=None, remote=None, key=None
|
||||
)
|
||||
|
||||
class TestPerf:
|
||||
def test_perf_status(self, b, mock_send):
|
||||
mock_send.return_value = {"profile": "auto"}
|
||||
assert b.perf.status() == {"profile": "auto"}
|
||||
mock_send.assert_called_once_with("perf.status", {}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_perf_set_profile(self, b, mock_send):
|
||||
mock_send.return_value = {"profile": "gentle"}
|
||||
assert b.perf.set_profile("gentle") == {"profile": "gentle"}
|
||||
mock_send.assert_called_once_with("perf.set_profile", {"profile": "gentle"}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_perf_job_status(self, b, mock_send):
|
||||
mock_send.return_value = {"phase": "done"}
|
||||
assert b.perf.job_status("j1") == {"phase": "done"}
|
||||
mock_send.assert_called_once_with("jobs.status", {"jobId": "j1"}, profile=None, remote=None, key=None)
|
||||
|
||||
class TestExtension:
|
||||
def test_extension_reload(self, b, mock_send):
|
||||
b.extension.reload()
|
||||
mock_send.assert_called_once_with("extension.reload", {}, profile=None, remote=None, key=None)
|
||||
|
||||
class TestSession:
|
||||
def test_session_save(self, b, mock_send):
|
||||
b.session.save("work")
|
||||
mock_send.assert_called_once_with("session.save", {"name": "work"}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_session_load(self, b, mock_send):
|
||||
b.session.load("work")
|
||||
mock_send.assert_called_once_with(
|
||||
"session.load",
|
||||
{"name": "work", "gentleMode": "auto", "discardBackgroundTabs": False, "lazy": False, "eagerTabs": 10},
|
||||
profile=None, remote=None, key=None,
|
||||
)
|
||||
|
||||
def test_session_load_background(self, b, mock_send):
|
||||
mock_send.return_value = {"jobId": "j1"}
|
||||
assert b.session.load_background("work") == {"jobId": "j1"}
|
||||
assert mock_send.call_args[0][1]["__background"] is True
|
||||
|
||||
def test_session_list(self, b, mock_send):
|
||||
mock_send.return_value = [{"name": "saved", "tabs": 3, "savedAt": 1712707200000}]
|
||||
|
||||
result = b.session_list()
|
||||
result = b.session.list()
|
||||
|
||||
assert result == [{"name": "saved", "tabs": 3, "savedAt": 1712707200000}]
|
||||
mock_send.assert_called_once_with("session.list", {}, profile=None, remote=None, key=None)
|
||||
@@ -762,7 +849,7 @@ class TestSession:
|
||||
[{"name": "first", "tabs": 2, "savedAt": 1712707200000}],
|
||||
[{"name": "second", "tabs": 5, "savedAt": 1712707300000}],
|
||||
]
|
||||
result = b.session_list()
|
||||
result = b.session.list()
|
||||
|
||||
assert result == [
|
||||
{"name": "first", "tabs": 2, "savedAt": 1712707200000, "browser": "uuid-1"},
|
||||
@@ -863,7 +950,6 @@ class TestTabModel:
|
||||
with pytest.raises(RuntimeError, match="not bound"):
|
||||
tab.close()
|
||||
|
||||
|
||||
# ── Group model ───────────────────────────────────────────────────────────────
|
||||
|
||||
class TestGroupModel:
|
||||
|
||||
Reference in New Issue
Block a user