feat(cli): improve tab and window tree rendering
- 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.
This commit is contained in:
@@ -18,6 +18,7 @@ TAB_DATA = {
|
||||
"id": 10,
|
||||
"windowId": 1,
|
||||
"active": True,
|
||||
"index": 3,
|
||||
"title": "Example",
|
||||
"url": "https://example.com",
|
||||
"groupId": None,
|
||||
@@ -73,6 +74,15 @@ class TestBrowserCLIInit:
|
||||
assert b.remote == "browser-host.example:443"
|
||||
assert b.key == "agent"
|
||||
|
||||
def test_tab_factory_preserves_index(self):
|
||||
tab = BrowserCLI().tab_from(TAB_DATA)
|
||||
assert tab.index == 3
|
||||
|
||||
def test_tab_factory_defaults_missing_index_to_zero(self):
|
||||
data = {key: value for key, value in TAB_DATA.items() if key != "index"}
|
||||
tab = BrowserCLI().tab_from(data)
|
||||
assert tab.index == 0
|
||||
|
||||
def test_namespaces_present_and_bound(self):
|
||||
b = BrowserCLI()
|
||||
for name in ("nav", "tabs", "groups", "windows", "dom", "extract",
|
||||
|
||||
@@ -7,6 +7,7 @@ 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
|
||||
|
||||
@@ -47,12 +48,94 @@ def test_nav_open_reuse_navigates_existing_tab_instead_of_opening_new():
|
||||
("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", return_value=[]):
|
||||
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")):
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
from os import terminal_size
|
||||
|
||||
from rich.console import Console
|
||||
from rich.tree import Tree
|
||||
|
||||
from browser_cli.commands import rendering
|
||||
|
||||
def test_shorten_uses_ellipsis():
|
||||
assert rendering.shorten("abcdef", 4) == "abc…"
|
||||
assert rendering.shorten("abc", 4) == "abc"
|
||||
|
||||
def test_terminal_width_prefers_shell_width_when_rich_is_redirected(monkeypatch):
|
||||
monkeypatch.setattr(rendering.shutil, "get_terminal_size", lambda fallback: terminal_size((140, 20)))
|
||||
assert rendering.terminal_width(Console(width=80)) == 140
|
||||
|
||||
def test_tab_tree_label_is_reusable_no_wrap_text():
|
||||
tab = type("Tab", (), {"id": 1, "title": "abcdef", "active": True, "url": "https://example.com"})()
|
||||
label = rendering.tab_tree_label(tab, title_limit=4, show_urls=True, url_limit=12)
|
||||
assert label.no_wrap is True
|
||||
assert label.overflow == "ellipsis"
|
||||
assert "abc…" in label.plain
|
||||
assert "https://exa…" in label.plain
|
||||
|
||||
def test_print_tree_uses_detected_width(monkeypatch):
|
||||
widths = []
|
||||
class CapturingConsole(Console):
|
||||
def __init__(self, *args, **kwargs):
|
||||
widths.append(kwargs.get("width"))
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(rendering, "Console", CapturingConsole)
|
||||
monkeypatch.setattr(rendering, "terminal_width", lambda console=None: 132)
|
||||
rendering.print_tree(Tree("Root"))
|
||||
assert widths == [132]
|
||||
Reference in New Issue
Block a user