"""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 import browser_cli from browser_cli import BrowserCLI, BrowserCounts, Tab, Group from browser_cli.client import BrowserNotConnected, BrowserTarget # ── Helpers ─────────────────────────────────────────────────────────────────── TAB_DATA = { "id": 10, "windowId": 1, "active": True, "title": "Example", "url": "https://example.com", "groupId": None, } GROUP_DATA = { "id": 42, "title": "Work", "color": "blue", "collapsed": False, "tabCount": 3, } @pytest.fixture() def mock_send(): """Patch send_command for the duration of one test. BrowserCLI._cmd calls the `send_command` name that was imported into browser_cli/__init__.py, so we must patch it there, not in the client module. """ 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: def test_default_browser_is_none(self): b = BrowserCLI() assert b._browser is None def test_named_browser_stored(self): b = BrowserCLI(browser="chrome") assert b._browser == "chrome" def test_remote_options_stored(self): b = BrowserCLI(browser="work", remote="host:8765", key=None) assert b._browser == "work" assert b._remote == "host:8765" def test_public_target_properties(self): b = BrowserCLI(browser="work", remote="browser-host.example:443", key="agent") assert b.browser == "work" 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") result = b.command("custom.command", {"value": 1}) assert result == {"ok": True} mock_send.assert_called_once_with( "custom.command", {"value": 1}, profile="work", remote="browser-host.example:443", key="agent", ) def test_raw_command_defaults_args_to_empty_dict(self, b, mock_send): b.command("custom.command") mock_send.assert_called_once_with("custom.command", {}, profile=None, remote=None, key=None) # ── Internal factories ──────────────────────────────────────────────────────── class TestMakeTab: def test_all_fields(self, b): tab = b._make_tab(TAB_DATA) assert tab.id == 10 assert tab.window_id == 1 assert tab.active is True assert tab.title == "Example" assert tab.url == "https://example.com" assert tab.group_id is None def test_bound_to_browser(self, b): tab = b._make_tab(TAB_DATA) assert tab._browser is b def test_missing_optional_fields_use_defaults(self, b): tab = b._make_tab({"id": 1}) assert tab.window_id == 0 assert tab.active is False assert tab.title == "" assert tab.url == "" assert tab.group_id is None def test_group_id_set(self, b): tab = b._make_tab({**TAB_DATA, "groupId": 99}) assert tab.group_id == 99 class TestMakeGroup: def test_all_fields(self, b): group = b._make_group(GROUP_DATA) assert group.id == 42 assert group.title == "Work" assert group.color == "blue" assert group.collapsed is False assert group.tab_count == 3 def test_bound_to_browser(self, b): group = b._make_group(GROUP_DATA) assert group._browser is b def test_missing_optional_fields_use_defaults(self, b): group = b._make_group({"id": 5}) assert group.title == "" assert group.color == "" assert group.collapsed is False assert group.tab_count == 0 # ── Navigation ──────────────────────────────────────────────────────────────── class TestNavigation: def test_open(self, b, mock_send): b.nav.open("https://example.com") mock_send.assert_called_once_with( "navigate.open", {"url": "https://example.com", "background": False, "window": None, "group": None}, profile=None, remote=None, key=None, ) def test_open_background(self, b, mock_send): 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.nav.open("https://x.com", group="Work") assert mock_send.call_args[0][1]["group"] == "Work" def test_tabs_open_returns_bound_tab(self, b, mock_send): mock_send.return_value = {"id": 123, "url": "https://example.com"} tab = b.tabs.open("https://example.com", background=True) assert tab.id == 123 assert tab.url == "https://example.com" assert tab._browser is b mock_send.assert_called_once_with( "navigate.open", {"url": "https://example.com", "background": True, "window": None, "group": None}, profile=None, remote=None, key=None, ) def test_tabs_open_wait_uses_open_wait(self, b, mock_send): mock_send.return_value = TAB_DATA tab = b.tabs.open("https://example.com", wait=True, timeout=1.5) assert tab.id == 10 mock_send.assert_called_once_with( "navigate.open_wait", {"url": "https://example.com", "timeout": 1500, "background": False, "window": None, "group": None}, profile=None, remote=None, key=None, ) 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.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.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.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.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.nav.forward(tab_id=3) mock_send.assert_called_once_with("navigate.forward", {"tabId": 3}, profile=None, remote=None, key=None) 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_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.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.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.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"] def test_unknown_engine_raises(self, b): with pytest.raises(ValueError, match="Unknown search engine"): b.nav.search("nonexistent", "query") def test_search_alias_ddg(self, b, mock_send): 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.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() 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): 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() 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() == [] def test_tabs_list_none_response(self, b, mock_send): mock_send.return_value = None 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 def test_tabs_close_non_dict_response(self, b, mock_send): mock_send.return_value = None 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) mock_send.assert_called_once_with( "tabs.close", {"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 mock_send.assert_called_once_with( "tabs.close", {"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 mock_send.assert_called_once_with( "tabs.close", {"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) 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_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_tabs_active_in_window(self, b, mock_send): mock_send.return_value = TAB_DATA 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_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.tabs.active_in_window(1) def test_tabs_filter(self, b, mock_send): mock_send.return_value = [TAB_DATA] 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") == [] 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) 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]) 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 def test_tabs_list_multi_browser_annotates_browser_and_binds_actions(self, b, mock_send): with patch( "browser_cli.active_browser_targets", return_value=[ BrowserTarget("default", "uuid-1", "/tmp/uuid-1.sock"), BrowserTarget("work", "work", "/tmp/work.sock"), ], ): mock_send.side_effect = [ [TAB_DATA], [{**TAB_DATA, "id": 11}], None, ] tabs = b.tabs.list() tabs[1].close() assert [tab.browser for tab in tabs] == ["uuid-1", "work"] assert [tab.id for tab in tabs] == [10, 11] assert mock_send.call_args_list == [ call("tabs.list", {}, profile="default"), call("tabs.list", {}, profile="work"), call("tabs.close", {"tabId": 11}, profile="work", remote=None, key=None), ] def test_tabs_list_remote_uses_only_requested_remote_and_binds_actions(self, mock_send): b = BrowserCLI(remote="host:8765", key=None) 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")], ): 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", key=None), call("tabs.close", {"tabId": 10}, profile="work", remote="host:8765", key=None), ] def test_tabs_list_remote_bound_actions_preserve_key(self, mock_send): b = BrowserCLI(remote="browser-host.example", key="agent") with patch( "browser_cli.remote_browser_targets", return_value=[BrowserTarget("work", "browser-host.example:work", "", remote="browser-host.example")], ): mock_send.side_effect = [[TAB_DATA], None] tabs = b.tabs.list() tabs[0].close() assert mock_send.call_args_list == [ call("tabs.list", {}, profile="work", remote="browser-host.example", key="agent"), call("tabs.close", {"tabId": 10}, profile="work", remote="browser-host.example", key="agent"), ] def test_tabs_active_returns_active_tab(self, b, mock_send): mock_send.side_effect = [[TAB_DATA], TAB_DATA] tabs = b.tabs.list() active = b.tabs.active() assert tabs[0].id == 10 assert active.id == 10 assert mock_send.call_args_list == [ call("tabs.list", {}, profile=None, remote=None, key=None), call("tabs.status", {"tabId": None}, profile=None, remote=None, key=None), ] 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.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 assert missing is None assert closed_by_object == 1 assert closed_by_id == 1 assert mock_send.call_args_list == [ 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, "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_tabs_count_multi_browser_returns_browser_counts(self, b, mock_send): with patch( "browser_cli.active_browser_targets", return_value=[ BrowserTarget("default", "uuid-1", "/tmp/uuid-1.sock"), BrowserTarget("work", "work", "/tmp/work.sock"), ], ): mock_send.side_effect = [3, 4] result = b.tabs.count("github") assert result == BrowserCounts(total=7, by_browser={"uuid-1": 3, "work": 4}) assert mock_send.call_args_list == [ call("tabs.count", {"pattern": "github"}, profile="default"), call("tabs.count", {"pattern": "github"}, profile="work"), ] def test_tabs_query(self, b, mock_send): mock_send.return_value = [TAB_DATA] 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 = "" assert b.tabs.html(10) == "" def test_tabs_html_none(self, b, mock_send): mock_send.return_value = None assert b.tabs.html() == "" def test_tabs_dedupe(self, b, mock_send): mock_send.return_value = {"closed": 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 def test_tabs_sort(self, b, mock_send): 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 def test_tabs_close_inactive(self, b, mock_send): mock_send.return_value = {"closed": 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 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.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.groups.list() == [] def test_group_tabs(self, b, mock_send): mock_send.return_value = [TAB_DATA] 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.groups.count() == 7 def test_group_list_multi_browser_annotates_browser_and_binds_actions(self, b, mock_send): with patch( "browser_cli.active_browser_targets", return_value=[ BrowserTarget("default", "uuid-1", "/tmp/uuid-1.sock"), BrowserTarget("work", "work", "/tmp/work.sock"), ], ): mock_send.side_effect = [ [GROUP_DATA], [{**GROUP_DATA, "id": 99, "title": "Later"}], None, ] groups = b.groups.list() groups[1].close() assert [group.browser for group in groups] == ["uuid-1", "work"] assert [group.id for group in groups] == [42, 99] assert mock_send.call_args_list == [ call("group.list", {}, profile="default"), call("group.list", {}, profile="work"), call("group.close", {"groupId": 99}, profile="work", remote=None, key=None), ] def test_group_list_remote_uses_only_requested_remote_and_binds_actions(self, mock_send): b = BrowserCLI(remote="host:8765", key=None) 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")], ): mock_send.side_effect = [[GROUP_DATA], None] groups = b.groups.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", key=None), call("group.close", {"groupId": 42}, profile="work", remote="host:8765", key=None), ] def test_group_list_remote_bound_actions_preserve_key(self, mock_send): b = BrowserCLI(remote="browser-host.example", key="agent") with patch( "browser_cli.remote_browser_targets", return_value=[BrowserTarget("work", "browser-host.example:work", "", remote="browser-host.example")], ): mock_send.side_effect = [[GROUP_DATA], None] groups = b.groups.list() groups[0].close() assert mock_send.call_args_list == [ call("group.list", {}, profile="work", remote="browser-host.example", key="agent"), call("group.close", {"groupId": 42}, profile="work", remote="browser-host.example", key="agent"), ] def test_group_count_multi_browser_returns_browser_counts(self, b, mock_send): with patch( "browser_cli.active_browser_targets", return_value=[ BrowserTarget("default", "uuid-1", "/tmp/uuid-1.sock"), BrowserTarget("work", "work", "/tmp/work.sock"), ], ): mock_send.side_effect = [2, 5] 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.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.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.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.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.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 ) def test_group_add_tab_non_dict_response(self, b, mock_send): mock_send.return_value = 55 assert b.groups.add_tab(42) == 55 def test_group_move_forward(self, b, mock_send): 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( "browser_cli.active_browser_targets", return_value=[ BrowserTarget("default", "uuid-1", "/tmp/uuid-1.sock"), BrowserTarget("work", "work", "/tmp/work.sock"), ], ): mock_send.side_effect = [ [{"id": 1, "tabCount": 2, "state": "normal"}], [{"id": 2, "tabCount": 3, "state": "maximized"}], ] result = b.windows.list() assert result == [ {"id": 1, "tabCount": 2, "state": "normal", "browser": "uuid-1"}, {"id": 2, "tabCount": 3, "state": "maximized", "browser": "work"}, ] def test_windows_open_without_url(self, b, mock_send): mock_send.return_value = {"id": 5} result = b.windows.open() assert result == {"id": 5} mock_send.assert_called_once_with("windows.open", {"url": None}, profile=None, remote=None, key=None) def test_windows_open_with_url(self, b, mock_send): mock_send.return_value = {"id": 9} 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) 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.dom.wait_for("#done", timeout=2.5, visible=True, tab_id=10) assert result == {"selector": "#done", "found": True} mock_send.assert_called_once_with( "dom.wait_for", {"selector": "#done", "timeout": 2500, "visible": True, "hidden": False, "tabId": 10}, profile=None, remote=None, 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() assert result == [{"name": "saved", "tabs": 3, "savedAt": 1712707200000}] mock_send.assert_called_once_with("session.list", {}, profile=None, remote=None, key=None) def test_session_list_multi_browser_adds_browser(self, b, mock_send): with patch( "browser_cli.active_browser_targets", return_value=[ BrowserTarget("default", "uuid-1", "/tmp/uuid-1.sock"), BrowserTarget("work", "work", "/tmp/work.sock"), ], ): mock_send.side_effect = [ [{"name": "first", "tabs": 2, "savedAt": 1712707200000}], [{"name": "second", "tabs": 5, "savedAt": 1712707300000}], ] result = b.session.list() assert result == [ {"name": "first", "tabs": 2, "savedAt": 1712707200000, "browser": "uuid-1"}, {"name": "second", "tabs": 5, "savedAt": 1712707300000, "browser": "work"}, ] assert mock_send.call_args_list == [ call("session.list", {}, profile="default"), call("session.list", {}, profile="work"), ] # ── Tab model ───────────────────────────────────────────────────────────────── class TestTabModel: @pytest.fixture() def tab(self, b, mock_send): return b._make_tab(TAB_DATA) def test_close(self, tab, mock_send): tab.close() mock_send.assert_called_once_with("tabs.close", {"tabId": 10}, profile=None, remote=None, key=None) def test_activate(self, tab, mock_send): tab.activate() mock_send.assert_called_once_with("tabs.active", {"tabId": 10}, profile=None, remote=None, key=None) def test_reload(self, tab, mock_send): tab.reload() mock_send.assert_called_once_with("navigate.reload", {"tabId": 10}, profile=None, remote=None, key=None) def test_hard_reload(self, tab, mock_send): tab.hard_reload() mock_send.assert_called_once_with("navigate.hard_reload", {"tabId": 10}, profile=None, remote=None, key=None) def test_move_forward(self, tab, mock_send): tab.move(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_move_to_group(self, tab, mock_send): tab.move(group_id=5) assert mock_send.call_args[0][1]["groupId"] == 5 def test_html(self, tab, mock_send): mock_send.return_value = "" assert tab.html() == "" mock_send.assert_called_once_with("tabs.html", {"tabId": 10}, profile=None, remote=None, key=None) def test_open(self, tab, mock_send): tab.open("https://new.example.com") mock_send.assert_called_once_with( "navigate.to", {"tabId": 10, "url": "https://new.example.com"}, profile=None, remote=None, key=None ) def test_screenshot(self, tab, mock_send): mock_send.return_value = {"dataUrl": "data:image/png;base64,abc"} assert tab.screenshot() == "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 ) def test_pin_unpin(self, tab, mock_send): tab.pin() tab.unpin() assert mock_send.call_args_list == [ call("tabs.pin", {"tabId": 10}, profile=None, remote=None, key=None), call("tabs.unpin", {"tabId": 10}, profile=None, remote=None, key=None), ] def test_refresh(self, tab, mock_send): mock_send.return_value = {**TAB_DATA, "title": "Fresh"} fresh = tab.refresh() assert fresh.title == "Fresh" mock_send.assert_called_once_with("tabs.status", {"tabId": 10}, profile=None, remote=None, key=None) def test_wait_for_load_and_watch_url(self, tab, mock_send): mock_send.side_effect = [TAB_DATA, TAB_DATA] tab.wait_for_load(timeout=1.5, ready_state="interactive") tab.watch_url("example", timeout=2) assert mock_send.call_args_list == [ call("navigate.wait", {"tabId": 10, "timeout": 1500, "readyState": "interactive"}, profile=None, remote=None, key=None), call("tabs.watch_url", {"pattern": "example", "tabId": 10, "timeout": 2000}, profile=None, remote=None, key=None), ] def test_open_background_changes_same_tab(self, tab, mock_send): tab.open("https://new.example.com", background=True) mock_send.assert_called_once_with( "navigate.to", {"tabId": 10, "url": "https://new.example.com"}, profile=None, remote=None, key=None, ) def test_unbound_raises(self): tab = Tab(id=1, window_id=0, active=False, title="", url="") with pytest.raises(RuntimeError, match="not bound"): tab.close() # ── Group model ─────────────────────────────────────────────────────────────── class TestGroupModel: @pytest.fixture() def group(self, b, mock_send): return b._make_group(GROUP_DATA) def test_close(self, group, mock_send): group.close() mock_send.assert_called_once_with("group.close", {"groupId": 42}, profile=None, remote=None, key=None) def test_tabs(self, group, mock_send): mock_send.return_value = [TAB_DATA] tabs = group.tabs() assert isinstance(tabs[0], Tab) mock_send.assert_called_once_with("group.tabs", {"groupId": 42}, profile=None, remote=None, key=None) def test_move_forward(self, group, mock_send): group.move(forward=True) mock_send.assert_called_once_with( "group.move", {"group": "42", "forward": True, "backward": False}, profile=None, remote=None, key=None ) def test_move_backward(self, group, mock_send): group.move(backward=True) assert mock_send.call_args[0][1]["backward"] is True def test_add_tab(self, group, mock_send): mock_send.return_value = {"tabId": 77} result = group.add_tab("https://example.com") assert result == 77 def test_add_tab_no_url(self, group, mock_send): mock_send.return_value = {"tabId": 78} group.add_tab() assert mock_send.call_args[0][1]["url"] is None def test_unbound_raises(self): group = Group(id=1, title="x", color="", collapsed=False, tab_count=0) with pytest.raises(RuntimeError, match="not bound"): group.close()