Files
browser-cli/tests/test_commands_cli.py
T
daniel156161 5cec57e06d
Testing / remote-protocol-compat (0.9.3) (push) Successful in 40s
Testing / remote-protocol-compat (0.9.5) (push) Successful in 38s
Testing / test (push) Failing after 1m3s
Package Extension / package-extension (push) Successful in 29s
Build & Publish Package / publish (push) Successful in 33s
feat!: harden raw browser control and packaging
- Add safe-by-default policy gates for raw command surfaces: command, script, and serve-http /command.

- Require explicit opt-ins for page reads, browser control, and high-risk commands such as dom.eval, storage.*, and screenshots.

- Remove all cookies support from CLI, SDK, extension commands, permissions, constants, docs, and tests.

- Add diagnostic, events, watch, workspace, remote, raw command, script, HTTP gateway, tree-view, session import/export, and extension info/capability commands.

- Add Chrome Web Store packaging that strips manifest.key while keeping local packages with a stable native-messaging extension ID.

- Bump browser-cli and extension version to 0.14.1 and cover the new behavior with pytest and extension packaging tests.

BREAKING CHANGE: cookies commands and the b.cookies SDK namespace have been removed; generic raw command execution now blocks non-safe commands unless explicitly allowed.
2026-06-14 14:33:15 +02:00

947 lines
38 KiB
Python

"""
Unit tests for CLI command modules.
All send_command calls are mocked — no live browser required.
Uses Click's CliRunner to exercise the CLI output formatting paths.
"""
from unittest.mock import patch, MagicMock
import pytest
from click.testing import CliRunner
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _run(group, args, return_value):
"""Invoke a Click group/command with a mocked send_command 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.send_command", side_effect=exc):
result = CliRunner().invoke(group, args)
return result
# ---------------------------------------------------------------------------
# dom commands
# ---------------------------------------------------------------------------
from browser_cli.commands.dom import dom_group
def test_cli_dom_query_no_results():
result = _run(dom_group, ["query", "span.nothing"], [])
assert result.exit_code == 0
assert "No elements found" in result.output
def test_cli_dom_query_with_results():
elements = [{"tag": "div", "text": "hello", "attrs": {"class": "foo"}}]
result = _run(dom_group, ["query", "div"], elements)
assert result.exit_code == 0
assert "div" in result.output
def test_cli_dom_click():
result = _run(dom_group, ["click", "button#submit"], None)
assert result.exit_code == 0
assert "Clicked" in result.output
def test_cli_dom_type():
result = _run(dom_group, ["type", "input#name", "pytest"], None)
assert result.exit_code == 0
assert "Typed into" in result.output
def test_cli_dom_attr():
result = _run(dom_group, ["attr", "a", "href"], ["https://example.com"])
assert result.exit_code == 0
assert "https://example.com" in result.output
def test_cli_dom_attr_empty():
result = _run(dom_group, ["attr", "span", "href"], [])
assert result.exit_code == 0
def test_cli_dom_text():
result = _run(dom_group, ["text", "h1"], ["Example Domain"])
assert result.exit_code == 0
assert "Example Domain" in result.output
def test_cli_dom_text_empty():
result = _run(dom_group, ["text", "h9"], [])
assert result.exit_code == 0
def test_cli_dom_exists_true():
result = _run(dom_group, ["exists", "html"], True)
assert result.exit_code == 0
assert "exists" in result.output
def test_cli_dom_exists_false():
result = _run(dom_group, ["exists", "#no-such"], False)
assert result.exit_code != 0
assert "not found" in result.output
def test_cli_dom_scroll_coord():
result = _run(dom_group, ["scroll", "--x", "0", "--y", "500"], None)
assert result.exit_code == 0
assert "Scrolled" in result.output
def test_cli_dom_scroll_selector():
result = _run(dom_group, ["scroll", "footer"], None)
assert result.exit_code == 0
assert "Scrolled" in result.output
def test_cli_dom_eval_string():
result = _run(dom_group, ["eval", "document.title"], "My Page")
assert result.exit_code == 0
assert "My Page" in result.output
def test_cli_dom_eval_null():
result = _run(dom_group, ["eval", "void 0"], None)
assert result.exit_code == 0
assert "null" in result.output
def test_cli_dom_eval_dict():
result = _run(dom_group, ["eval", "({a:1})"], {"a": 1})
assert result.exit_code == 0
assert '"a"' in result.output
def test_cli_dom_wait_for():
result = _run(dom_group, ["wait-for", "html"], None)
assert result.exit_code == 0
assert "Ready" in result.output
def test_cli_dom_wait_for_visible():
result = _run(dom_group, ["wait-for", "html", "--visible"], None)
assert result.exit_code == 0
assert "visible" in result.output
def test_cli_dom_wait_for_hidden():
result = _run(dom_group, ["wait-for", "#spinner", "--hidden"], None)
assert result.exit_code == 0
assert "hidden" in result.output
def test_cli_dom_key():
result = _run(dom_group, ["key", "Enter"], None)
assert result.exit_code == 0
assert "Key 'Enter'" in result.output
def test_cli_dom_key_with_selector():
result = _run(dom_group, ["key", "Tab", "--selector", "input"], None)
assert result.exit_code == 0
assert "input" in result.output
def test_cli_dom_hover():
result = _run(dom_group, ["hover", ".menu"], None)
assert result.exit_code == 0
assert "Hovered" in result.output
def test_cli_dom_check():
result = _run(dom_group, ["check", "#accept"], None)
assert result.exit_code == 0
assert "Checked" in result.output
def test_cli_dom_uncheck():
result = _run(dom_group, ["uncheck", "#accept"], None)
assert result.exit_code == 0
assert "Unchecked" in result.output
def test_cli_dom_clear():
result = _run(dom_group, ["clear", "input#q"], None)
assert result.exit_code == 0
assert "Cleared" in result.output
def test_cli_dom_focus():
result = _run(dom_group, ["focus", "input#q"], None)
assert result.exit_code == 0
assert "Focused" in result.output
def test_cli_dom_submit():
result = _run(dom_group, ["submit", "form"], None)
assert result.exit_code == 0
assert "Submitted" in result.output
def test_cli_dom_select():
result = _run(dom_group, ["select", "#lang", "en"], None)
assert result.exit_code == 0
assert "Selected" in result.output
def test_cli_dom_poll():
result = _run(dom_group, ["poll", "#status", "ready"], {"value": "ready"})
assert result.exit_code == 0
assert "Matched" in result.output
# ---------------------------------------------------------------------------
# page commands
# ---------------------------------------------------------------------------
from browser_cli.commands.page import page_group
def test_cli_page_info_basic():
info = {"title": "Example", "url": "https://example.com", "readyState": "complete", "lang": "en", "meta": {}}
result = _run(page_group, ["info"], info)
assert result.exit_code == 0
assert "Example" in result.output
assert "https://example.com" in result.output
def test_cli_page_info_with_meta():
info = {"title": "My Page", "url": "https://example.com", "readyState": "complete", "lang": "de",
"meta": {"description": "A test page", "author": "Tester"}}
result = _run(page_group, ["info"], info)
assert result.exit_code == 0
assert "description" in result.output
assert "A test page" in result.output
def test_cli_page_info_empty_response():
result = _run(page_group, ["info"], {})
assert result.exit_code == 0
# ---------------------------------------------------------------------------
# storage commands
# ---------------------------------------------------------------------------
from browser_cli.commands.storage import storage_group
def test_cli_storage_get_null():
result = _run(storage_group, ["get", "mykey"], None)
assert result.exit_code == 0
assert "null" in result.output
def test_cli_storage_get_string():
result = _run(storage_group, ["get", "mykey"], "hello-world")
assert result.exit_code == 0
assert "hello-world" in result.output
def test_cli_storage_get_dict():
result = _run(storage_group, ["get"], {"k1": "v1", "k2": "v2"})
assert result.exit_code == 0
assert "k1" in result.output
def test_cli_storage_get_session():
result = _run(storage_group, ["get", "key", "--type", "session"], "ses-val")
assert result.exit_code == 0
assert "ses-val" in result.output
def test_cli_storage_set():
result = _run(storage_group, ["set", "mykey", "myvalue"], None)
assert result.exit_code == 0
assert "Set" in result.output
assert "mykey" in result.output
def test_cli_storage_set_session():
result = _run(storage_group, ["set", "sk", "sv", "--type", "session"], None)
assert result.exit_code == 0
assert "session" in result.output
# ---------------------------------------------------------------------------
# perf commands
# ---------------------------------------------------------------------------
from browser_cli.commands.perf import perf_group
def test_cli_perf_status_no_jobs():
status = {"performanceProfile": "auto", "audible": False,
"throttle": {"batchSize": 10, "pauseMs": 50, "mode": "auto"}, "jobs": []}
result = _run(perf_group, ["status"], status)
assert result.exit_code == 0
assert "auto" in result.output
assert "No running jobs" in result.output
def test_cli_perf_status_with_jobs():
status = {
"performanceProfile": "gentle",
"audible": True,
"throttle": {"batchSize": 5, "pauseMs": 200, "mode": "gentle"},
"jobs": [
{"id": "j1", "command": "session.load", "status": "running",
"current": 3, "total": 10, "percent": 30, "phase": "open_tabs"},
],
}
result = _run(perf_group, ["status"], status)
assert result.exit_code == 0
assert "j1" in result.output
assert "session.load" in result.output
assert "30%" in result.output
def test_cli_perf_profile_set():
result = _run(perf_group, ["profile", "normal"], {"performanceProfile": "normal"})
assert result.exit_code == 0
assert "normal" in result.output
def test_cli_perf_profile_gentle():
result = _run(perf_group, ["profile", "gentle"], {"performanceProfile": "gentle"})
assert result.exit_code == 0
assert "gentle" in result.output
def test_cli_perf_profile_ultra():
result = _run(perf_group, ["profile", "ultra"], {"performanceProfile": "ultra"})
assert result.exit_code == 0
assert "ultra" in result.output
# ---------------------------------------------------------------------------
# navigate commands
# ---------------------------------------------------------------------------
from browser_cli.commands.navigate import nav_group
def test_cli_nav_open():
with patch("browser_cli.send_command", return_value={"id": 42, "url": "https://example.com"}) as send_command:
result = CliRunner().invoke(nav_group, ["open", "https://example.com"])
assert result.exit_code == 0
assert "Opened" in result.output
send_command.assert_called_once_with(
"navigate.open",
{"url": "https://example.com", "background": True, "focus": False, "window": None, "group": None},
profile=None,
remote=None,
key=None,
)
def test_cli_nav_open_has_no_bg_option():
result = CliRunner().invoke(nav_group, ["open", "--help"])
assert result.exit_code == 0
assert "--bg" not in result.output
def test_cli_nav_open_wait_has_no_bg_option():
result = CliRunner().invoke(nav_group, ["open-wait", "--help"])
assert result.exit_code == 0
assert "--bg" not in result.output
def test_cli_nav_open_focus_is_explicit():
with patch("browser_cli.send_command", return_value={"id": 42}) as send_command:
result = CliRunner().invoke(nav_group, ["open", "https://example.com", "--focus"])
assert result.exit_code == 0
send_command.assert_called_once_with(
"navigate.open",
{"url": "https://example.com", "background": False, "focus": True, "window": None, "group": None},
profile=None,
remote=None,
key=None,
)
def test_cli_nav_open_with_group():
result = _run(nav_group, ["open", "https://example.com", "--group", "work"], {"id": 42})
assert result.exit_code == 0
assert "work" in result.output
def test_cli_nav_open_with_window():
result = _run(nav_group, ["open", "https://example.com", "--window", "main"], {"id": 42})
assert result.exit_code == 0
assert "main" in result.output
def test_cli_nav_reload():
result = _run(nav_group, ["reload"], None)
assert result.exit_code == 0
assert "Reloaded" in result.output
def test_cli_nav_hard_reload():
result = _run(nav_group, ["hard-reload"], None)
assert result.exit_code == 0
assert "Hard reloaded" in result.output
def test_cli_nav_back():
result = _run(nav_group, ["back"], None)
assert result.exit_code == 0
assert "back" in result.output.lower()
def test_cli_nav_forward():
result = _run(nav_group, ["forward"], None)
assert result.exit_code == 0
assert "forward" in result.output.lower()
def test_cli_nav_focus_found():
result = _run(nav_group, ["focus", "example.com"], {"url": "https://example.com"})
assert result.exit_code == 0
assert "Focused" in result.output
def test_cli_nav_focus_not_found():
result = _run(nav_group, ["focus", "no-match"], None)
assert result.exit_code == 0
assert "No tab found" in result.output
def test_cli_nav_open_wait():
result = _run(nav_group, ["open-wait", "https://example.com"],
{"id": 1, "title": "Example Domain", "url": "https://example.com"})
assert result.exit_code == 0
assert "Loaded" in result.output
assert "Example Domain" in result.output
def test_cli_nav_open_wait_no_title():
result = _run(nav_group, ["open-wait", "https://example.com"], {"id": 1})
assert result.exit_code == 0
assert "Loaded" in result.output
def test_cli_nav_wait():
result = _run(nav_group, ["wait"], {"id": 1, "url": "https://example.com", "title": "Example"})
assert result.exit_code == 0
assert "Ready" in result.output
# ---------------------------------------------------------------------------
# search commands
# ---------------------------------------------------------------------------
from browser_cli.commands.search import search_group
def test_cli_search_has_no_bg_option():
result = CliRunner().invoke(search_group, ["google", "--help"])
assert result.exit_code == 0
assert "--bg" not in result.output
# ---------------------------------------------------------------------------
# navigate commands — with tab_id argument
# ---------------------------------------------------------------------------
def test_cli_nav_reload_with_tab_id():
result = _run(nav_group, ["reload", "42"], None)
assert result.exit_code == 0
def test_cli_nav_back_with_tab_id():
result = _run(nav_group, ["back", "42"], None)
assert result.exit_code == 0
def test_cli_nav_forward_with_tab_id():
result = _run(nav_group, ["forward", "42"], None)
assert result.exit_code == 0
# ---------------------------------------------------------------------------
# tabs commands (CLI module) — single-browser mode
# ---------------------------------------------------------------------------
from browser_cli.commands.tabs import tabs_group
_TABS_MOD = "browser_cli.commands.tabs"
_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.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.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.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.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.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.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.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.send_command", return_value=None):
result = CliRunner().invoke(tabs_group, ["active", "42"])
assert result.exit_code == 0
assert "42" in result.output
def test_cli_tabs_status():
tab = {"id": 5, "windowId": 1, "active": True, "muted": False,
"title": "MyPage", "url": "https://my.page"}
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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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
def test_cli_tabs_screenshot_to_file(tmp_path):
import base64
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.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()
assert out.exists()
# ---------------------------------------------------------------------------
# groups commands (CLI module)
# ---------------------------------------------------------------------------
from browser_cli.commands.groups import group_group
_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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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
# ---------------------------------------------------------------------------
# session commands (CLI module)
# ---------------------------------------------------------------------------
from browser_cli.commands.session import session_group
_SESSION_MOD = "browser_cli.commands.session"
def test_cli_session_save():
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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.send_command", return_value=None):
result = CliRunner().invoke(session_group, ["auto-save", "off"])
assert result.exit_code == 0
assert "off" in result.output
# ---------------------------------------------------------------------------
# multi-browser error paths (None targets → "Cannot resolve" exit)
# ---------------------------------------------------------------------------
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")
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.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.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.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.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.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
# ---------------------------------------------------------------------------
# tabs screenshot error paths
# ---------------------------------------------------------------------------
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.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.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
# ---------------------------------------------------------------------------
# windows commands (CLI module)
# ---------------------------------------------------------------------------
from browser_cli.commands.windows import windows_group
_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.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.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.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.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.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.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.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
# ---------------------------------------------------------------------------
# commands/__init__.py error paths
# ---------------------------------------------------------------------------
from browser_cli.commands import client_from_ctx, handle_errors
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_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_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)