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:
+114
-138
@@ -13,27 +13,16 @@ from click.testing import CliRunner
|
||||
|
||||
def _run(group, args, return_value):
|
||||
"""Invoke a Click group/command with a mocked send_command return value."""
|
||||
with patch("browser_cli.commands.send_command", return_value=return_value):
|
||||
with patch("browser_cli.send_command", return_value=return_value):
|
||||
result = CliRunner().invoke(group, args)
|
||||
return result
|
||||
|
||||
def _run_error(group, args, exc):
|
||||
"""Invoke a Click group/command where send_command raises exc."""
|
||||
with patch("browser_cli.commands.send_command", side_effect=exc):
|
||||
with patch("browser_cli.send_command", side_effect=exc):
|
||||
result = CliRunner().invoke(group, args)
|
||||
return result
|
||||
|
||||
def _run_no_multi(group, args, return_value, module_path=None):
|
||||
"""Invoke a command patching send_command AND _multi_browser_targets → single-browser mode."""
|
||||
ctx1 = patch("browser_cli.commands.send_command", return_value=return_value)
|
||||
if module_path:
|
||||
ctx2 = patch(f"{module_path}._multi_browser_targets", return_value=[])
|
||||
with ctx1, ctx2:
|
||||
return CliRunner().invoke(group, args)
|
||||
else:
|
||||
with ctx1:
|
||||
return CliRunner().invoke(group, args)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# dom commands
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -419,7 +408,7 @@ def test_cli_nav_open_wait_no_title():
|
||||
assert "Loaded" in result.output
|
||||
|
||||
def test_cli_nav_wait():
|
||||
result = _run(nav_group, ["wait"], {"url": "https://example.com", "title": "Example"})
|
||||
result = _run(nav_group, ["wait"], {"id": 1, "url": "https://example.com", "title": "Example"})
|
||||
assert result.exit_code == 0
|
||||
assert "Ready" in result.output
|
||||
|
||||
@@ -451,50 +440,50 @@ _SAMPLE_TAB = {"id": 1, "windowId": 1, "active": True, "muted": False,
|
||||
"title": "Example", "url": "https://example.com", "groupId": -1}
|
||||
|
||||
def test_cli_tabs_list_empty():
|
||||
with patch("browser_cli.commands.send_command", return_value=[]), \
|
||||
patch(f"{_TABS_MOD}._multi_browser_targets", return_value=[]):
|
||||
with patch("browser_cli.send_command", return_value=[]), \
|
||||
patch("browser_cli.active_browser_targets", return_value=[]):
|
||||
result = CliRunner().invoke(tabs_group, ["list"])
|
||||
assert result.exit_code == 0
|
||||
assert "No tabs found" in result.output
|
||||
|
||||
def test_cli_tabs_list_with_tabs():
|
||||
with patch("browser_cli.commands.send_command", return_value=[_SAMPLE_TAB]), \
|
||||
patch(f"{_TABS_MOD}._multi_browser_targets", return_value=[]):
|
||||
with patch("browser_cli.send_command", return_value=[_SAMPLE_TAB]), \
|
||||
patch("browser_cli.active_browser_targets", return_value=[]):
|
||||
result = CliRunner().invoke(tabs_group, ["list"])
|
||||
assert result.exit_code == 0
|
||||
assert "example.com" in result.output
|
||||
|
||||
def test_cli_tabs_close_by_id():
|
||||
with patch("browser_cli.commands.send_command", return_value={"closed": 1}):
|
||||
with patch("browser_cli.send_command", return_value={"closed": 1}):
|
||||
result = CliRunner().invoke(tabs_group, ["close", "42"])
|
||||
assert result.exit_code == 0
|
||||
assert "Closed 1" in result.output
|
||||
|
||||
def test_cli_tabs_close_inactive():
|
||||
with patch("browser_cli.commands.send_command", return_value={"closed": 3}):
|
||||
with patch("browser_cli.send_command", return_value={"closed": 3}):
|
||||
result = CliRunner().invoke(tabs_group, ["close", "--inactive"])
|
||||
assert result.exit_code == 0
|
||||
assert "Closed 3" in result.output
|
||||
|
||||
def test_cli_tabs_close_duplicates():
|
||||
with patch("browser_cli.commands.send_command", return_value={"closed": 2}):
|
||||
with patch("browser_cli.send_command", return_value={"closed": 2}):
|
||||
result = CliRunner().invoke(tabs_group, ["close", "--duplicates"])
|
||||
assert result.exit_code == 0
|
||||
assert "Closed 2" in result.output
|
||||
|
||||
def test_cli_tabs_move_forward():
|
||||
with patch("browser_cli.commands.send_command", return_value=None):
|
||||
with patch("browser_cli.send_command", return_value=None):
|
||||
result = CliRunner().invoke(tabs_group, ["move", "42", "--forward"])
|
||||
assert result.exit_code == 0
|
||||
assert "Tab moved" in result.output
|
||||
|
||||
def test_cli_tabs_move_to_window():
|
||||
with patch("browser_cli.commands.send_command", return_value=None):
|
||||
with patch("browser_cli.send_command", return_value=None):
|
||||
result = CliRunner().invoke(tabs_group, ["move", "42", "--window", "2"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
def test_cli_tabs_active():
|
||||
with patch("browser_cli.commands.send_command", return_value=None):
|
||||
with patch("browser_cli.send_command", return_value=None):
|
||||
result = CliRunner().invoke(tabs_group, ["active", "42"])
|
||||
assert result.exit_code == 0
|
||||
assert "42" in result.output
|
||||
@@ -502,100 +491,100 @@ def test_cli_tabs_active():
|
||||
def test_cli_tabs_status():
|
||||
tab = {"id": 5, "windowId": 1, "active": True, "muted": False,
|
||||
"title": "MyPage", "url": "https://my.page"}
|
||||
with patch("browser_cli.commands.send_command", return_value=tab):
|
||||
with patch("browser_cli.send_command", return_value=tab):
|
||||
result = CliRunner().invoke(tabs_group, ["status"])
|
||||
assert result.exit_code == 0
|
||||
assert "MyPage" in result.output
|
||||
|
||||
def test_cli_tabs_filter():
|
||||
with patch("browser_cli.commands.send_command", return_value=[_SAMPLE_TAB]):
|
||||
with patch("browser_cli.send_command", return_value=[_SAMPLE_TAB]):
|
||||
result = CliRunner().invoke(tabs_group, ["filter", "example"])
|
||||
assert result.exit_code == 0
|
||||
assert "example.com" in result.output
|
||||
|
||||
def test_cli_tabs_filter_empty():
|
||||
with patch("browser_cli.commands.send_command", return_value=[]):
|
||||
with patch("browser_cli.send_command", return_value=[]):
|
||||
result = CliRunner().invoke(tabs_group, ["filter", "nope"])
|
||||
assert result.exit_code == 0
|
||||
assert "No tabs" in result.output
|
||||
|
||||
def test_cli_tabs_count():
|
||||
with patch("browser_cli.commands.send_command", return_value=7), \
|
||||
patch(f"{_TABS_MOD}._multi_browser_targets", return_value=[]):
|
||||
with patch("browser_cli.send_command", return_value=7), \
|
||||
patch("browser_cli.active_browser_targets", return_value=[]):
|
||||
result = CliRunner().invoke(tabs_group, ["count"])
|
||||
assert result.exit_code == 0
|
||||
assert "7" in result.output
|
||||
|
||||
def test_cli_tabs_count_with_pattern():
|
||||
with patch("browser_cli.commands.send_command", return_value=3), \
|
||||
patch(f"{_TABS_MOD}._multi_browser_targets", return_value=[]):
|
||||
with patch("browser_cli.send_command", return_value=3), \
|
||||
patch("browser_cli.active_browser_targets", return_value=[]):
|
||||
result = CliRunner().invoke(tabs_group, ["count", "http"])
|
||||
assert result.exit_code == 0
|
||||
assert "3" in result.output
|
||||
assert "http" in result.output
|
||||
|
||||
def test_cli_tabs_query():
|
||||
with patch("browser_cli.commands.send_command", return_value=[_SAMPLE_TAB]):
|
||||
with patch("browser_cli.send_command", return_value=[_SAMPLE_TAB]):
|
||||
result = CliRunner().invoke(tabs_group, ["query", "example"])
|
||||
assert result.exit_code == 0
|
||||
assert "example.com" in result.output
|
||||
|
||||
def test_cli_tabs_html():
|
||||
with patch("browser_cli.commands.send_command", return_value="<html><body>hello</body></html>"):
|
||||
with patch("browser_cli.send_command", return_value="<html><body>hello</body></html>"):
|
||||
result = CliRunner().invoke(tabs_group, ["html"])
|
||||
assert result.exit_code == 0
|
||||
assert "hello" in result.output
|
||||
|
||||
def test_cli_tabs_dedupe():
|
||||
with patch("browser_cli.commands.send_command", return_value={"closed": 4}):
|
||||
with patch("browser_cli.send_command", return_value={"closed": 4}):
|
||||
result = CliRunner().invoke(tabs_group, ["dedupe"])
|
||||
assert result.exit_code == 0
|
||||
assert "4 duplicate" in result.output
|
||||
|
||||
def test_cli_tabs_sort():
|
||||
with patch("browser_cli.commands.send_command", return_value=None):
|
||||
with patch("browser_cli.send_command", return_value=None):
|
||||
result = CliRunner().invoke(tabs_group, ["sort", "--by", "title"])
|
||||
assert result.exit_code == 0
|
||||
assert "title" in result.output
|
||||
|
||||
def test_cli_tabs_merge_windows():
|
||||
with patch("browser_cli.commands.send_command", return_value={"moved": 2}):
|
||||
with patch("browser_cli.send_command", return_value={"moved": 2}):
|
||||
result = CliRunner().invoke(tabs_group, ["merge-windows"])
|
||||
assert result.exit_code == 0
|
||||
assert "2" in result.output
|
||||
|
||||
def test_cli_tabs_mute():
|
||||
with patch("browser_cli.commands.send_command", return_value={"tabId": 5}):
|
||||
with patch("browser_cli.send_command", return_value={"tabId": 5}):
|
||||
result = CliRunner().invoke(tabs_group, ["mute", "5"])
|
||||
assert result.exit_code == 0
|
||||
assert "Muted tab 5" in result.output
|
||||
|
||||
def test_cli_tabs_unmute():
|
||||
with patch("browser_cli.commands.send_command", return_value={"tabId": 5}):
|
||||
with patch("browser_cli.send_command", return_value={"tabId": 5}):
|
||||
result = CliRunner().invoke(tabs_group, ["unmute", "5"])
|
||||
assert result.exit_code == 0
|
||||
assert "Unmuted tab 5" in result.output
|
||||
|
||||
def test_cli_tabs_pin():
|
||||
with patch("browser_cli.commands.send_command", return_value={"tabId": 5}):
|
||||
with patch("browser_cli.send_command", return_value={"tabId": 5}):
|
||||
result = CliRunner().invoke(tabs_group, ["pin", "5"])
|
||||
assert result.exit_code == 0
|
||||
assert "Pinned tab 5" in result.output
|
||||
|
||||
def test_cli_tabs_unpin():
|
||||
with patch("browser_cli.commands.send_command", return_value={"tabId": 5}):
|
||||
with patch("browser_cli.send_command", return_value={"tabId": 5}):
|
||||
result = CliRunner().invoke(tabs_group, ["unpin", "5"])
|
||||
assert result.exit_code == 0
|
||||
assert "Unpinned tab 5" in result.output
|
||||
|
||||
def test_cli_tabs_watch_url():
|
||||
with patch("browser_cli.commands.send_command", return_value={"url": "https://done.com"}):
|
||||
with patch("browser_cli.send_command", return_value={"id": 1, "url": "https://done.com"}):
|
||||
result = CliRunner().invoke(tabs_group, ["watch-url", "done\\.com"])
|
||||
assert result.exit_code == 0
|
||||
assert "done.com" in result.output
|
||||
|
||||
def test_cli_tabs_screenshot_stdout():
|
||||
with patch("browser_cli.commands.send_command", return_value={"dataUrl": "data:image/png;base64,abc"}):
|
||||
with patch("browser_cli.send_command", return_value={"dataUrl": "data:image/png;base64,abc"}):
|
||||
result = CliRunner().invoke(tabs_group, ["screenshot"])
|
||||
assert result.exit_code == 0
|
||||
assert "data:image/png" in result.output
|
||||
@@ -605,7 +594,7 @@ def test_cli_tabs_screenshot_to_file(tmp_path):
|
||||
png_bytes = b"\x89PNG\r\n\x1a\n" + b"\x00" * 8 # minimal header
|
||||
data_url = "data:image/png;base64," + base64.b64encode(png_bytes).decode()
|
||||
out = tmp_path / "shot.png"
|
||||
with patch("browser_cli.commands.send_command", return_value={"dataUrl": data_url}):
|
||||
with patch("browser_cli.send_command", return_value={"dataUrl": data_url}):
|
||||
result = CliRunner().invoke(tabs_group, ["screenshot", str(out)])
|
||||
assert result.exit_code == 0
|
||||
assert "saved" in result.output.lower()
|
||||
@@ -622,95 +611,95 @@ _GROUPS_MOD = "browser_cli.commands.groups"
|
||||
_SAMPLE_GROUP = {"id": 10, "title": "Work", "color": "blue", "collapsed": False, "tabCount": 3}
|
||||
|
||||
def test_cli_groups_list_empty():
|
||||
with patch("browser_cli.commands.send_command", return_value=[]), \
|
||||
patch(f"{_GROUPS_MOD}._multi_browser_targets", return_value=[]):
|
||||
with patch("browser_cli.send_command", return_value=[]), \
|
||||
patch("browser_cli.active_browser_targets", return_value=[]):
|
||||
result = CliRunner().invoke(group_group, ["list"])
|
||||
assert result.exit_code == 0
|
||||
assert "No groups found" in result.output
|
||||
|
||||
def test_cli_groups_list_with_groups():
|
||||
with patch("browser_cli.commands.send_command", return_value=[_SAMPLE_GROUP]), \
|
||||
patch(f"{_GROUPS_MOD}._multi_browser_targets", return_value=[]):
|
||||
with patch("browser_cli.send_command", return_value=[_SAMPLE_GROUP]), \
|
||||
patch("browser_cli.active_browser_targets", return_value=[]):
|
||||
result = CliRunner().invoke(group_group, ["list"])
|
||||
assert result.exit_code == 0
|
||||
assert "Work" in result.output
|
||||
|
||||
def test_cli_groups_tabs():
|
||||
tabs = [_SAMPLE_TAB]
|
||||
with patch("browser_cli.commands.send_command", return_value=tabs):
|
||||
with patch("browser_cli.send_command", return_value=tabs):
|
||||
result = CliRunner().invoke(group_group, ["tabs", "10"])
|
||||
assert result.exit_code == 0
|
||||
assert "example.com" in result.output
|
||||
|
||||
def test_cli_groups_tabs_empty():
|
||||
with patch("browser_cli.commands.send_command", return_value=[]):
|
||||
with patch("browser_cli.send_command", return_value=[]):
|
||||
result = CliRunner().invoke(group_group, ["tabs", "10"])
|
||||
assert result.exit_code == 0
|
||||
assert "No tabs" in result.output
|
||||
|
||||
def test_cli_groups_count():
|
||||
with patch("browser_cli.commands.send_command", return_value=5), \
|
||||
patch(f"{_GROUPS_MOD}._multi_browser_targets", return_value=[]):
|
||||
with patch("browser_cli.send_command", return_value=5), \
|
||||
patch("browser_cli.active_browser_targets", return_value=[]):
|
||||
result = CliRunner().invoke(group_group, ["count"])
|
||||
assert result.exit_code == 0
|
||||
assert "5" in result.output
|
||||
|
||||
def test_cli_groups_query_empty():
|
||||
with patch("browser_cli.commands.send_command", return_value=[]):
|
||||
with patch("browser_cli.send_command", return_value=[]):
|
||||
result = CliRunner().invoke(group_group, ["query", "nothing"])
|
||||
assert result.exit_code == 0
|
||||
assert "No groups" in result.output
|
||||
|
||||
def test_cli_groups_query_found():
|
||||
with patch("browser_cli.commands.send_command", return_value=[_SAMPLE_GROUP]):
|
||||
with patch("browser_cli.send_command", return_value=[_SAMPLE_GROUP]):
|
||||
result = CliRunner().invoke(group_group, ["query", "Work"])
|
||||
assert result.exit_code == 0
|
||||
assert "Work" in result.output
|
||||
|
||||
def test_cli_groups_close():
|
||||
with patch("browser_cli.commands.send_command", return_value=None):
|
||||
with patch("browser_cli.send_command", return_value=None):
|
||||
result = CliRunner().invoke(group_group, ["close", "10"])
|
||||
assert result.exit_code == 0
|
||||
assert "10" in result.output
|
||||
|
||||
def test_cli_groups_create():
|
||||
with patch("browser_cli.commands.send_command", return_value={"id": 42}):
|
||||
with patch("browser_cli.send_command", return_value={"id": 42}):
|
||||
result = CliRunner().invoke(group_group, ["create", "Research"])
|
||||
assert result.exit_code == 0
|
||||
assert "Research" in result.output
|
||||
assert "42" in result.output
|
||||
|
||||
def test_cli_groups_add_tab_no_url():
|
||||
with patch("browser_cli.commands.send_command", return_value={"tabId": 7}):
|
||||
with patch("browser_cli.send_command", return_value={"tabId": 7}):
|
||||
result = CliRunner().invoke(group_group, ["add-tab", "Work"])
|
||||
assert result.exit_code == 0
|
||||
assert "Work" in result.output
|
||||
|
||||
def test_cli_groups_add_tab_with_url():
|
||||
with patch("browser_cli.commands.send_command", return_value={"tabId": 9}):
|
||||
with patch("browser_cli.send_command", return_value={"tabId": 9}):
|
||||
result = CliRunner().invoke(group_group, ["add-tab", "Work", "https://docs.example.com"])
|
||||
assert result.exit_code == 0
|
||||
assert "docs.example.com" in result.output
|
||||
|
||||
def test_cli_groups_move_forward():
|
||||
with patch("browser_cli.commands.send_command", return_value={"moved": True}):
|
||||
with patch("browser_cli.send_command", return_value={"moved": True}):
|
||||
result = CliRunner().invoke(group_group, ["move", "10", "--forward"])
|
||||
assert result.exit_code == 0
|
||||
assert "forward" in result.output
|
||||
|
||||
def test_cli_groups_move_backward():
|
||||
with patch("browser_cli.commands.send_command", return_value={"moved": True}):
|
||||
with patch("browser_cli.send_command", return_value={"moved": True}):
|
||||
result = CliRunner().invoke(group_group, ["move", "10", "--backward"])
|
||||
assert result.exit_code == 0
|
||||
assert "backward" in result.output
|
||||
|
||||
def test_cli_groups_move_no_direction():
|
||||
with patch("browser_cli.commands.send_command", return_value=None):
|
||||
with patch("browser_cli.send_command", return_value=None):
|
||||
result = CliRunner().invoke(group_group, ["move", "10"])
|
||||
assert result.exit_code != 0
|
||||
|
||||
def test_cli_groups_move_already_at_end():
|
||||
with patch("browser_cli.commands.send_command", return_value={"moved": False}):
|
||||
with patch("browser_cli.send_command", return_value={"moved": False}):
|
||||
result = CliRunner().invoke(group_group, ["move", "10", "--forward"])
|
||||
assert result.exit_code == 0
|
||||
assert "already at" in result.output
|
||||
@@ -724,103 +713,103 @@ from browser_cli.commands.session import session_group
|
||||
_SESSION_MOD = "browser_cli.commands.session"
|
||||
|
||||
def test_cli_session_save():
|
||||
with patch("browser_cli.commands.send_command", return_value={"tabs": 5}):
|
||||
with patch("browser_cli.send_command", return_value={"tabs": 5}):
|
||||
result = CliRunner().invoke(session_group, ["save", "work"])
|
||||
assert result.exit_code == 0
|
||||
assert "work" in result.output
|
||||
assert "5" in result.output
|
||||
|
||||
def test_cli_session_load():
|
||||
with patch("browser_cli.commands.send_command", return_value={"tabs": 8}):
|
||||
with patch("browser_cli.send_command", return_value={"tabs": 8}):
|
||||
result = CliRunner().invoke(session_group, ["load", "work"])
|
||||
assert result.exit_code == 0
|
||||
assert "work" in result.output
|
||||
assert "8" in result.output
|
||||
|
||||
def test_cli_session_load_background():
|
||||
with patch("browser_cli.commands.send_command", return_value={"jobId": "j-abc", "status": "running"}):
|
||||
with patch("browser_cli.send_command", return_value={"jobId": "j-abc", "status": "running"}):
|
||||
result = CliRunner().invoke(session_group, ["load", "work", "--background"])
|
||||
assert result.exit_code == 0
|
||||
assert "j-abc" in result.output
|
||||
|
||||
def test_cli_session_load_lazy():
|
||||
with patch("browser_cli.commands.send_command", return_value={"tabs": 20}):
|
||||
with patch("browser_cli.send_command", return_value={"tabs": 20}):
|
||||
result = CliRunner().invoke(session_group, ["load", "work", "--lazy", "--eager-tabs", "5"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
def test_cli_session_diff_has_changes():
|
||||
diff = {"added": ["https://new.com"], "removed": ["https://old.com"]}
|
||||
with patch("browser_cli.commands.send_command", return_value=diff):
|
||||
with patch("browser_cli.send_command", return_value=diff):
|
||||
result = CliRunner().invoke(session_group, ["diff", "a", "b"])
|
||||
assert result.exit_code == 0
|
||||
assert "new.com" in result.output
|
||||
assert "old.com" in result.output
|
||||
|
||||
def test_cli_session_diff_identical():
|
||||
with patch("browser_cli.commands.send_command", return_value={"added": [], "removed": []}):
|
||||
with patch("browser_cli.send_command", return_value={"added": [], "removed": []}):
|
||||
result = CliRunner().invoke(session_group, ["diff", "a", "b"])
|
||||
assert result.exit_code == 0
|
||||
assert "identical" in result.output
|
||||
|
||||
def test_cli_session_diff_no_data():
|
||||
with patch("browser_cli.commands.send_command", return_value=None):
|
||||
with patch("browser_cli.send_command", return_value=None):
|
||||
result = CliRunner().invoke(session_group, ["diff", "a", "b"])
|
||||
assert result.exit_code == 0
|
||||
assert "No diff" in result.output
|
||||
|
||||
def test_cli_session_list_empty():
|
||||
with patch("browser_cli.commands.send_command", return_value=[]), \
|
||||
patch(f"{_SESSION_MOD}._multi_browser_targets", return_value=[]):
|
||||
with patch("browser_cli.send_command", return_value=[]), \
|
||||
patch("browser_cli.active_browser_targets", return_value=[]):
|
||||
result = CliRunner().invoke(session_group, ["list"])
|
||||
assert result.exit_code == 0
|
||||
assert "No saved sessions" in result.output
|
||||
|
||||
def test_cli_session_list_with_sessions():
|
||||
sessions = [{"name": "work", "tabs": 3, "savedAt": 1700000000000}]
|
||||
with patch("browser_cli.commands.send_command", return_value=sessions), \
|
||||
patch(f"{_SESSION_MOD}._multi_browser_targets", return_value=[]):
|
||||
with patch("browser_cli.send_command", return_value=sessions), \
|
||||
patch("browser_cli.active_browser_targets", return_value=[]):
|
||||
result = CliRunner().invoke(session_group, ["list"])
|
||||
assert result.exit_code == 0
|
||||
assert "work" in result.output
|
||||
|
||||
def test_cli_session_remove():
|
||||
with patch("browser_cli.commands.send_command", return_value=None):
|
||||
with patch("browser_cli.send_command", return_value=None):
|
||||
result = CliRunner().invoke(session_group, ["remove", "old-session"])
|
||||
assert result.exit_code == 0
|
||||
assert "old-session" in result.output
|
||||
|
||||
def test_cli_session_job_status_running():
|
||||
with patch("browser_cli.commands.send_command", return_value={"status": "running", "percent": 42}):
|
||||
with patch("browser_cli.send_command", return_value={"status": "running", "percent": 42}):
|
||||
result = CliRunner().invoke(session_group, ["job-status", "j-xyz"])
|
||||
assert result.exit_code == 0
|
||||
assert "running" in result.output
|
||||
|
||||
def test_cli_session_job_status_with_error():
|
||||
with patch("browser_cli.commands.send_command", return_value={"status": "failed", "error": "something broke"}):
|
||||
with patch("browser_cli.send_command", return_value={"status": "failed", "error": "something broke"}):
|
||||
result = CliRunner().invoke(session_group, ["job-status", "j-bad"])
|
||||
assert result.exit_code == 0
|
||||
assert "something broke" in result.output
|
||||
|
||||
def test_cli_session_job_status_with_result():
|
||||
with patch("browser_cli.commands.send_command", return_value={"status": "done", "result": "Opened 5 tabs"}):
|
||||
with patch("browser_cli.send_command", return_value={"status": "done", "result": "Opened 5 tabs"}):
|
||||
result = CliRunner().invoke(session_group, ["job-status", "j-done"])
|
||||
assert result.exit_code == 0
|
||||
assert "Opened 5 tabs" in result.output
|
||||
|
||||
def test_cli_session_job_cancel():
|
||||
with patch("browser_cli.commands.send_command", return_value=None):
|
||||
with patch("browser_cli.send_command", return_value=None):
|
||||
result = CliRunner().invoke(session_group, ["job-cancel", "j-running"])
|
||||
assert result.exit_code == 0
|
||||
assert "j-running" in result.output
|
||||
|
||||
def test_cli_session_auto_save_on():
|
||||
with patch("browser_cli.commands.send_command", return_value=None):
|
||||
with patch("browser_cli.send_command", return_value=None):
|
||||
result = CliRunner().invoke(session_group, ["auto-save", "on"])
|
||||
assert result.exit_code == 0
|
||||
assert "on" in result.output
|
||||
|
||||
def test_cli_session_auto_save_off():
|
||||
with patch("browser_cli.commands.send_command", return_value=None):
|
||||
with patch("browser_cli.send_command", return_value=None):
|
||||
result = CliRunner().invoke(session_group, ["auto-save", "off"])
|
||||
assert result.exit_code == 0
|
||||
assert "off" in result.output
|
||||
@@ -829,7 +818,7 @@ def test_cli_session_auto_save_off():
|
||||
# multi-browser error paths (None targets → "Cannot resolve" exit)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
from browser_cli.client import BrowserTarget
|
||||
from browser_cli.client import BrowserNotConnected, BrowserTarget
|
||||
|
||||
def _fake_target(name="browser-a"):
|
||||
return BrowserTarget(profile=name, display_name=name, socket_path="/tmp/fake.sock")
|
||||
@@ -837,45 +826,40 @@ def _fake_target(name="browser-a"):
|
||||
def test_cli_tabs_list_multi_browser_all_none():
|
||||
"""If every multi-browser target returns None, show error and exit 1."""
|
||||
target = _fake_target()
|
||||
with patch("browser_cli.commands.send_command", return_value=None), \
|
||||
patch(f"{_TABS_MOD}._multi_browser_targets", return_value=[target]), \
|
||||
patch(f"{_TABS_MOD}._handle_multi", return_value=None):
|
||||
with patch("browser_cli.send_command", side_effect=BrowserNotConnected("gone")), \
|
||||
patch("browser_cli.active_browser_targets", return_value=[target, _fake_target("browser-b")]):
|
||||
result = CliRunner().invoke(tabs_group, ["list"])
|
||||
assert result.exit_code != 0
|
||||
|
||||
def test_cli_tabs_count_multi_browser_all_none():
|
||||
"""If every multi-browser count returns None, show error and exit 1."""
|
||||
target = _fake_target()
|
||||
with patch("browser_cli.commands.send_command", return_value=None), \
|
||||
patch(f"{_TABS_MOD}._multi_browser_targets", return_value=[target]), \
|
||||
patch(f"{_TABS_MOD}._handle_multi", return_value=None):
|
||||
with patch("browser_cli.send_command", side_effect=BrowserNotConnected("gone")), \
|
||||
patch("browser_cli.active_browser_targets", return_value=[target, _fake_target("browser-b")]):
|
||||
result = CliRunner().invoke(tabs_group, ["count"])
|
||||
assert result.exit_code != 0
|
||||
|
||||
def test_cli_groups_list_multi_browser_all_none():
|
||||
"""If every multi-browser group list returns None, exit 1."""
|
||||
target = _fake_target()
|
||||
with patch("browser_cli.commands.send_command", return_value=None), \
|
||||
patch(f"{_GROUPS_MOD}._multi_browser_targets", return_value=[target]), \
|
||||
patch(f"{_GROUPS_MOD}._handle_multi", return_value=None):
|
||||
with patch("browser_cli.send_command", side_effect=BrowserNotConnected("gone")), \
|
||||
patch("browser_cli.active_browser_targets", return_value=[target, _fake_target("browser-b")]):
|
||||
result = CliRunner().invoke(group_group, ["list"])
|
||||
assert result.exit_code != 0
|
||||
|
||||
def test_cli_groups_count_multi_browser_all_none():
|
||||
"""If every multi-browser group count returns None, exit 1."""
|
||||
target = _fake_target()
|
||||
with patch("browser_cli.commands.send_command", return_value=None), \
|
||||
patch(f"{_GROUPS_MOD}._multi_browser_targets", return_value=[target]), \
|
||||
patch(f"{_GROUPS_MOD}._handle_multi", return_value=None):
|
||||
with patch("browser_cli.send_command", side_effect=BrowserNotConnected("gone")), \
|
||||
patch("browser_cli.active_browser_targets", return_value=[target, _fake_target("browser-b")]):
|
||||
result = CliRunner().invoke(group_group, ["count"])
|
||||
assert result.exit_code != 0
|
||||
|
||||
def test_cli_session_list_multi_browser_all_none():
|
||||
"""If every multi-browser session list returns None, exit 1."""
|
||||
target = _fake_target()
|
||||
with patch("browser_cli.commands.send_command", return_value=None), \
|
||||
patch(f"{_SESSION_MOD}._multi_browser_targets", return_value=[target]), \
|
||||
patch(f"{_SESSION_MOD}._handle_multi", return_value=None):
|
||||
with patch("browser_cli.send_command", side_effect=BrowserNotConnected("gone")), \
|
||||
patch("browser_cli.active_browser_targets", return_value=[target, _fake_target("browser-b")]):
|
||||
result = CliRunner().invoke(session_group, ["list"])
|
||||
assert result.exit_code != 0
|
||||
|
||||
@@ -886,14 +870,14 @@ def test_cli_session_list_multi_browser_all_none():
|
||||
def test_cli_tabs_screenshot_bad_dataurl(tmp_path):
|
||||
"""Screenshot command exits non-zero when dataUrl has wrong format."""
|
||||
out = tmp_path / "bad.png"
|
||||
with patch("browser_cli.commands.send_command", return_value={"dataUrl": "not-a-dataurl"}):
|
||||
with patch("browser_cli.send_command", return_value={"dataUrl": "not-a-dataurl"}):
|
||||
result = CliRunner().invoke(tabs_group, ["screenshot", str(out)])
|
||||
assert result.exit_code != 0
|
||||
|
||||
def test_cli_tabs_screenshot_bad_base64(tmp_path):
|
||||
"""Screenshot command exits non-zero when base64 data is corrupt."""
|
||||
out = tmp_path / "bad.png"
|
||||
with patch("browser_cli.commands.send_command", return_value={"dataUrl": "data:image/png;base64,!!!NOT_VALID_BASE64!!!"}):
|
||||
with patch("browser_cli.send_command", return_value={"dataUrl": "data:image/png;base64,!!!NOT_VALID_BASE64!!!"}):
|
||||
result = CliRunner().invoke(tabs_group, ["screenshot", str(out)])
|
||||
assert result.exit_code != 0
|
||||
|
||||
@@ -908,48 +892,47 @@ _WINDOWS_MOD = "browser_cli.commands.windows"
|
||||
_SAMPLE_WINDOW = {"id": 1, "alias": "main", "tabCount": 5, "state": "normal"}
|
||||
|
||||
def test_cli_windows_list_empty():
|
||||
with patch("browser_cli.commands.send_command", return_value=[]), \
|
||||
patch(f"{_WINDOWS_MOD}._multi_browser_targets", return_value=[]):
|
||||
with patch("browser_cli.send_command", return_value=[]), \
|
||||
patch("browser_cli.active_browser_targets", return_value=[]):
|
||||
result = CliRunner().invoke(windows_group, ["list"])
|
||||
assert result.exit_code == 0
|
||||
assert "No windows found" in result.output
|
||||
|
||||
def test_cli_windows_list_with_windows():
|
||||
with patch("browser_cli.commands.send_command", return_value=[_SAMPLE_WINDOW]), \
|
||||
patch(f"{_WINDOWS_MOD}._multi_browser_targets", return_value=[]):
|
||||
with patch("browser_cli.send_command", return_value=[_SAMPLE_WINDOW]), \
|
||||
patch("browser_cli.active_browser_targets", return_value=[]):
|
||||
result = CliRunner().invoke(windows_group, ["list"])
|
||||
assert result.exit_code == 0
|
||||
assert "main" in result.output
|
||||
|
||||
def test_cli_windows_list_multi_all_none():
|
||||
target = _fake_target()
|
||||
with patch("browser_cli.commands.send_command", return_value=None), \
|
||||
patch(f"{_WINDOWS_MOD}._multi_browser_targets", return_value=[target]), \
|
||||
patch(f"{_WINDOWS_MOD}._handle_multi", return_value=None):
|
||||
with patch("browser_cli.send_command", side_effect=BrowserNotConnected("gone")), \
|
||||
patch("browser_cli.active_browser_targets", return_value=[target, _fake_target("browser-b")]):
|
||||
result = CliRunner().invoke(windows_group, ["list"])
|
||||
assert result.exit_code != 0
|
||||
|
||||
def test_cli_windows_rename():
|
||||
with patch("browser_cli.commands.send_command", return_value=None):
|
||||
with patch("browser_cli.send_command", return_value=None):
|
||||
result = CliRunner().invoke(windows_group, ["rename", "1", "work"])
|
||||
assert result.exit_code == 0
|
||||
assert "work" in result.output
|
||||
assert "1" in result.output
|
||||
|
||||
def test_cli_windows_close():
|
||||
with patch("browser_cli.commands.send_command", return_value=None):
|
||||
with patch("browser_cli.send_command", return_value=None):
|
||||
result = CliRunner().invoke(windows_group, ["close", "2"])
|
||||
assert result.exit_code == 0
|
||||
assert "2" in result.output
|
||||
|
||||
def test_cli_windows_open_no_url():
|
||||
with patch("browser_cli.commands.send_command", return_value={"id": 5}):
|
||||
with patch("browser_cli.send_command", return_value={"id": 5}):
|
||||
result = CliRunner().invoke(windows_group, ["open"])
|
||||
assert result.exit_code == 0
|
||||
assert "5" in result.output
|
||||
|
||||
def test_cli_windows_open_with_url():
|
||||
with patch("browser_cli.commands.send_command", return_value={"id": 6}):
|
||||
with patch("browser_cli.send_command", return_value={"id": 6}):
|
||||
result = CliRunner().invoke(windows_group, ["open", "https://example.com"])
|
||||
assert result.exit_code == 0
|
||||
assert "example.com" in result.output
|
||||
@@ -958,36 +941,29 @@ def test_cli_windows_open_with_url():
|
||||
# commands/__init__.py error paths
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
from browser_cli.commands import _handle, _handle_multi
|
||||
from browser_cli.client import BrowserNotConnected
|
||||
from browser_cli.commands import client_from_ctx, handle_errors
|
||||
|
||||
def test_handle_raises_system_exit_on_browser_not_connected():
|
||||
"""_handle converts BrowserNotConnected into SystemExit(1)."""
|
||||
with patch("browser_cli.commands.send_command", side_effect=BrowserNotConnected("no socket")):
|
||||
with pytest.raises(SystemExit):
|
||||
_handle("tabs.list")
|
||||
def test_handle_errors_converts_browser_not_connected_to_exit():
|
||||
"""handle_errors turns BrowserNotConnected into a clean SystemExit(1)."""
|
||||
with patch("browser_cli.send_command", side_effect=BrowserNotConnected("no socket")), \
|
||||
patch("browser_cli.active_browser_targets", return_value=[]):
|
||||
result = CliRunner().invoke(tabs_group, ["list"])
|
||||
assert result.exit_code == 1
|
||||
assert "no socket" in result.output
|
||||
|
||||
def test_handle_raises_system_exit_on_runtime_error():
|
||||
"""_handle converts RuntimeError into SystemExit(1)."""
|
||||
with patch("browser_cli.commands.send_command", side_effect=RuntimeError("browser blew up")):
|
||||
with pytest.raises(SystemExit):
|
||||
_handle("tabs.list")
|
||||
def test_handle_errors_converts_runtime_error_to_exit():
|
||||
"""handle_errors turns a browser RuntimeError into a clean SystemExit(1)."""
|
||||
with patch("browser_cli.send_command", side_effect=RuntimeError("browser blew up")), \
|
||||
patch("browser_cli.active_browser_targets", return_value=[]):
|
||||
result = CliRunner().invoke(tabs_group, ["list"])
|
||||
assert result.exit_code == 1
|
||||
assert "browser blew up" in result.output
|
||||
|
||||
def test_handle_multi_returns_none_on_error():
|
||||
"""_handle_multi silently returns None on BrowserNotConnected."""
|
||||
with patch("browser_cli.commands.send_command", side_effect=BrowserNotConnected("gone")):
|
||||
result = _handle_multi("tabs.list")
|
||||
assert result is None
|
||||
|
||||
def test_handle_multi_returns_none_on_runtime_error():
|
||||
"""_handle_multi silently returns None on RuntimeError."""
|
||||
with patch("browser_cli.commands.send_command", side_effect=RuntimeError("oops")):
|
||||
result = _handle_multi("tabs.list")
|
||||
assert result is None
|
||||
|
||||
def test_handle_multi_with_remote():
|
||||
"""_handle_multi routes through remote when remote arg is set."""
|
||||
with patch("browser_cli.commands.send_command", return_value={"ok": True}) as mock_send:
|
||||
result = _handle_multi("tabs.list", profile="brave", remote="host:8765")
|
||||
assert result == {"ok": True}
|
||||
mock_send.assert_called_once_with("tabs.list", {}, profile="brave", remote="host:8765")
|
||||
def test_client_from_ctx_forwards_global_options():
|
||||
"""client_from_ctx builds a BrowserCLI from the root --browser/--remote options."""
|
||||
from browser_cli.cli import main
|
||||
with patch("browser_cli.send_command", return_value=[]) as mock_send, \
|
||||
patch("browser_cli.active_browser_targets", return_value=[]):
|
||||
result = CliRunner().invoke(main, ["--browser", "brave", "--remote", "host:8765", "tabs", "list"])
|
||||
assert result.exit_code == 0
|
||||
mock_send.assert_called_once_with("tabs.list", {}, profile="brave", remote="host:8765", key=None)
|
||||
|
||||
Reference in New Issue
Block a user