0b43408a8d
- Add shared rendering helpers for width-aware tree labels, truncation, and no-wrap Rich text. - Preserve tab index and group window metadata through the extension and SDK factories. - Render tab trees in browser/window/index order with grouped tab details and optional shortened URLs. - Reuse the tab tree labels in window trees to keep output compact and consistent. - Cover legacy missing-index responses, grouped/collapsed tabs, URL display, and rendering helpers with tests.
189 lines
9.3 KiB
Python
189 lines
9.3 KiB
Python
import json
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from click.testing import CliRunner
|
|
|
|
import pytest
|
|
|
|
from browser_cli import BrowserCLI
|
|
from browser_cli.client import BrowserTarget
|
|
from browser_cli.cli import main
|
|
from browser_cli.command_security import CommandPolicy, assert_command_allowed, command_category
|
|
|
|
def test_extension_info_cli_renders_capabilities():
|
|
with patch("browser_cli.send_command", return_value={"version": "1.2.3", "capabilities": ["extension.info"]}):
|
|
result = CliRunner().invoke(main, ["extension", "info"])
|
|
assert result.exit_code == 0
|
|
assert "1.2.3" in result.output
|
|
assert "extension.info" in result.output
|
|
|
|
def test_script_runs_raw_commands(tmp_path: Path):
|
|
script = tmp_path / "workflow.json"
|
|
script.write_text(json.dumps([{"tabs.count": {"pattern": "example.com"}}]), encoding="utf-8")
|
|
with patch("browser_cli.send_command", return_value={"count": 2}) as send_command:
|
|
result = CliRunner().invoke(main, ["script", str(script), "--json"])
|
|
assert result.exit_code == 0
|
|
assert "tabs.count" in result.output
|
|
send_command.assert_called_once_with("tabs.count", {"pattern": "example.com"}, profile=None, remote=None, key=None)
|
|
|
|
def test_session_export_cli_prints_json():
|
|
with patch("browser_cli.send_command", return_value={"name": "work", "session": {"tabs": ["https://example.com"]}}):
|
|
result = CliRunner().invoke(main, ["session", "export", "work"])
|
|
assert result.exit_code == 0
|
|
assert '"name": "work"' in result.output
|
|
|
|
def test_nav_open_reuse_navigates_existing_tab_instead_of_opening_new():
|
|
calls = []
|
|
|
|
def sender(command, args=None, **kwargs):
|
|
calls.append((command, args))
|
|
if command == "tabs.list":
|
|
return [{"id": 7, "windowId": 1, "active": False, "muted": False, "title": "Example", "url": "https://example.com"}]
|
|
return {}
|
|
|
|
BrowserCLI(browser="testing", _command_sender=sender).nav.open("https://example.com", reuse=True)
|
|
assert calls == [
|
|
("tabs.list", {}),
|
|
("navigate.to", {"tabId": 7, "url": "https://example.com"}),
|
|
]
|
|
|
|
def _tree_sender(tabs, groups):
|
|
def sender(command, args=None, **kwargs):
|
|
if command == "tabs.list":
|
|
return tabs
|
|
if command == "group.list":
|
|
return groups
|
|
return []
|
|
return sender
|
|
|
|
def test_tabs_tree_command_available():
|
|
with patch("browser_cli.send_command", side_effect=_tree_sender([], [])):
|
|
result = CliRunner().invoke(main, ["tabs", "tree"])
|
|
assert result.exit_code == 0
|
|
assert "Tabs" in result.output
|
|
|
|
def test_tabs_tree_handles_tabs_without_index_from_older_extension():
|
|
tabs = [{
|
|
"id": 7,
|
|
"windowId": 1,
|
|
"active": True,
|
|
"muted": False,
|
|
"title": "Example",
|
|
"url": "https://example.com",
|
|
"groupId": None,
|
|
}]
|
|
with patch("browser_cli.send_command", side_effect=_tree_sender(tabs, [])):
|
|
result = CliRunner().invoke(main, ["tabs", "tree"])
|
|
assert result.exit_code == 0
|
|
assert "Example" in result.output
|
|
|
|
def test_tabs_tree_preserves_window_tab_order_and_truncates_long_lines():
|
|
tabs = [
|
|
{"id": 1, "windowId": 1, "index": 0, "active": False, "title": "Before", "url": "https://example.com/before", "groupId": None},
|
|
{"id": 2, "windowId": 1, "index": 1, "active": False, "title": "[Gold] Grouped", "url": "https://example.com/grouped", "groupId": 20},
|
|
{"id": 3, "windowId": 1, "index": 2, "active": False, "title": "After", "url": "https://example.com/" + "x" * 200, "groupId": None},
|
|
]
|
|
groups = [{"id": 20, "title": "Group Name", "color": "blue", "collapsed": False, "tabCount": 1, "windowId": 1}]
|
|
with patch("browser_cli.send_command", side_effect=_tree_sender(tabs, groups)):
|
|
result = CliRunner().invoke(main, ["tabs", "tree"])
|
|
assert result.exit_code == 0
|
|
output = result.output
|
|
assert output.index("Before") < output.index("Group Name") < output.index("[Gold] Grouped") < output.index("After")
|
|
assert "https://example.com/before" not in output
|
|
assert "https://example.com/grouped" not in output
|
|
assert "https://example.com/" + "x" * 200 not in output
|
|
|
|
def test_tabs_tree_adds_each_browser_node_only_once():
|
|
tabs = [
|
|
{"id": 1, "windowId": 1, "index": 0, "active": False, "title": "One", "url": "https://example.com/one", "groupId": None, "browser": "work"},
|
|
{"id": 2, "windowId": 1, "index": 1, "active": False, "title": "Two", "url": "https://example.com/two", "groupId": None, "browser": "work"},
|
|
]
|
|
targets = [
|
|
BrowserTarget("work", "work", "/tmp/work.sock"),
|
|
BrowserTarget("personal", "personal", "/tmp/personal.sock"),
|
|
]
|
|
with patch("browser_cli.active_browser_targets", return_value=targets), \
|
|
patch("browser_cli.send_command", side_effect=_tree_sender(tabs, [])):
|
|
result = CliRunner().invoke(main, ["tabs", "tree"])
|
|
assert result.exit_code == 0
|
|
assert result.output.count("work") == 1
|
|
assert result.output.count("personal") == 1
|
|
assert "One" in result.output
|
|
assert "Two" in result.output
|
|
|
|
def test_tabs_tree_shows_tabs_inside_collapsed_browser_groups():
|
|
tabs = [
|
|
{"id": 1, "windowId": 1, "index": 0, "active": False, "title": "Before", "url": "https://example.com/before", "groupId": None},
|
|
{"id": 2, "windowId": 1, "index": 1, "active": False, "title": "Hidden", "url": "https://example.com/hidden", "groupId": 20},
|
|
{"id": 3, "windowId": 1, "index": 2, "active": False, "title": "After", "url": "https://example.com/after", "groupId": None},
|
|
]
|
|
groups = [{"id": 20, "title": "Collapsed Group", "color": "orange", "collapsed": True, "tabCount": 1, "windowId": 1}]
|
|
with patch("browser_cli.send_command", side_effect=_tree_sender(tabs, groups)):
|
|
result = CliRunner().invoke(main, ["tabs", "tree"])
|
|
assert result.exit_code == 0
|
|
assert "Collapsed Group" in result.output
|
|
assert "1 tab" in result.output
|
|
assert "collapsed" in result.output
|
|
assert "Hidden" in result.output
|
|
|
|
def test_tabs_tree_can_show_shortened_urls_on_request():
|
|
tabs = [{"id": 1, "windowId": 1, "index": 0, "active": False, "title": "Long URL", "url": "https://example.com/" + "x" * 200, "groupId": None}]
|
|
with patch("browser_cli.send_command", side_effect=_tree_sender(tabs, [])):
|
|
result = CliRunner().invoke(main, ["tabs", "tree", "--urls"])
|
|
assert result.exit_code == 0
|
|
assert "https://example.com/" in result.output
|
|
assert "https://example.com/" + "x" * 200 not in result.output
|
|
assert "…" in result.output
|
|
|
|
def test_doctor_command_reports_connection_failure_cleanly():
|
|
with patch("browser_cli.commands.doctor.active_browser_targets", return_value=[]), \
|
|
patch("browser_cli.send_command", side_effect=RuntimeError("no browser")):
|
|
result = CliRunner().invoke(main, ["doctor"])
|
|
assert result.exit_code == 1
|
|
assert "Connection" in result.output
|
|
|
|
def test_serve_http_no_auth_rejected_on_public_host():
|
|
result = CliRunner().invoke(main, ["serve-http", "--host", "0.0.0.0", "--no-auth"])
|
|
assert result.exit_code != 0
|
|
assert "--no-auth is only allowed on loopback" in result.output
|
|
|
|
def test_raw_command_blocks_dangerous_by_default():
|
|
result = CliRunner().invoke(main, ["command", "dom.eval", '{"code":"document.title"}'])
|
|
assert result.exit_code != 0
|
|
assert "blocked by default" in result.output
|
|
|
|
def test_raw_command_allows_dangerous_with_explicit_flag():
|
|
with patch("browser_cli.send_command", return_value="Example") as send_command:
|
|
result = CliRunner().invoke(main, ["command", "--allow-dangerous", "dom.eval", '{"code":"document.title"}'])
|
|
assert result.exit_code == 0
|
|
send_command.assert_called_once_with("dom.eval", {"code": "document.title"}, profile=None, remote=None, key=None)
|
|
|
|
def test_script_blocks_control_without_explicit_flag(tmp_path: Path):
|
|
script = tmp_path / "workflow.json"
|
|
script.write_text(json.dumps([{"navigate.open": {"url": "https://example.com"}}]), encoding="utf-8")
|
|
result = CliRunner().invoke(main, ["script", str(script), "--json"])
|
|
assert result.exit_code != 0
|
|
assert "blocked by default" in result.output
|
|
|
|
def test_script_allows_control_with_explicit_flag(tmp_path: Path):
|
|
script = tmp_path / "workflow.json"
|
|
script.write_text(json.dumps([{"navigate.open": {"url": "https://example.com"}}]), encoding="utf-8")
|
|
with patch("browser_cli.send_command", return_value={}) as send_command:
|
|
result = CliRunner().invoke(main, ["script", str(script), "--json", "--allow-control"])
|
|
assert result.exit_code == 0
|
|
send_command.assert_called_once_with("navigate.open", {"url": "https://example.com"}, profile=None, remote=None, key=None)
|
|
|
|
def test_command_policy_categories_and_flags():
|
|
assert command_category("tabs.list") == "safe"
|
|
assert command_category("extract.text") == "read-page"
|
|
assert command_category("dom.click") == "control"
|
|
assert command_category("storage.get") == "dangerous"
|
|
assert_command_allowed("tabs.list", CommandPolicy())
|
|
with pytest.raises(PermissionError):
|
|
assert_command_allowed("extract.text", CommandPolicy())
|
|
assert_command_allowed("extract.text", CommandPolicy(allow_read_page=True))
|
|
with pytest.raises(PermissionError):
|
|
assert_command_allowed("storage.get", CommandPolicy(allow_read_page=True, allow_control=True))
|
|
assert_command_allowed("storage.get", CommandPolicy(allow_dangerous=True))
|