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:
@@ -0,0 +1,55 @@
|
|||||||
|
"""Reusable rendering helpers for CLI command modules."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.text import Text
|
||||||
|
from rich.tree import Tree
|
||||||
|
|
||||||
|
def shorten(value: str | None, limit: int) -> str:
|
||||||
|
"""Return *value* shortened to *limit* cells-ish, using an ellipsis."""
|
||||||
|
value = value or ""
|
||||||
|
return value if len(value) <= limit else value[:max(0, limit - 1)] + "…"
|
||||||
|
|
||||||
|
def terminal_width(console: Console | None = None, *, fallback: int = 120) -> int:
|
||||||
|
"""Best-effort terminal width for interactive and redirected output.
|
||||||
|
|
||||||
|
Rich falls back to 80 columns when stdout is redirected. browser-cli output is
|
||||||
|
often piped into files for inspection, so also consult ``shutil``/``COLUMNS``
|
||||||
|
and prefer the wider value.
|
||||||
|
"""
|
||||||
|
rich_width = (console.width if console is not None else 0) or 0
|
||||||
|
shell_width = shutil.get_terminal_size((fallback, 20)).columns
|
||||||
|
return max(rich_width, shell_width)
|
||||||
|
|
||||||
|
def tree_title_limit(*, console: Console | None = None, show_browser: bool = False, show_urls: bool = False) -> int:
|
||||||
|
"""Title width for tree labels, reserving space for branches/IDs/metadata."""
|
||||||
|
reserve = 48 if show_urls else 32
|
||||||
|
if show_browser:
|
||||||
|
reserve += 4
|
||||||
|
return max(50, terminal_width(console) - reserve)
|
||||||
|
|
||||||
|
def tree_url_limit(title_limit: int, *, console: Console | None = None) -> int:
|
||||||
|
"""URL width for tree labels when URLs are displayed."""
|
||||||
|
return max(35, terminal_width(console) - title_limit - 40)
|
||||||
|
|
||||||
|
def no_wrap_text() -> Text:
|
||||||
|
"""Text configured for one-line tree labels with edge ellipsis."""
|
||||||
|
return Text(no_wrap=True, overflow="ellipsis")
|
||||||
|
|
||||||
|
def tab_tree_label(tab, *, title_limit: int, show_urls: bool = False, url_limit: int = 55) -> Text:
|
||||||
|
"""Reusable one-line label for a browser tab in tree views."""
|
||||||
|
label = no_wrap_text()
|
||||||
|
label.append(f"[{tab.id}] ", style="dim")
|
||||||
|
label.append(shorten(tab.title or "(untitled)", title_limit))
|
||||||
|
if tab.active:
|
||||||
|
label.append(" *", style="green")
|
||||||
|
if show_urls and tab.url:
|
||||||
|
label.append(" — ", style="dim")
|
||||||
|
label.append(shorten(tab.url, url_limit), style="dim")
|
||||||
|
return label
|
||||||
|
|
||||||
|
def print_tree(tree: Tree, *, console: Console | None = None) -> None:
|
||||||
|
"""Render a Rich tree using the detected full terminal width."""
|
||||||
|
Console(width=terminal_width(console)).print(tree)
|
||||||
@@ -2,12 +2,38 @@ import base64
|
|||||||
import binascii
|
import binascii
|
||||||
import click
|
import click
|
||||||
from browser_cli.commands import client_from_ctx, gentle_mode_option, handle_errors, print_counts, tab_option
|
from browser_cli.commands import client_from_ctx, gentle_mode_option, handle_errors, print_counts, tab_option
|
||||||
|
from browser_cli.commands.rendering import no_wrap_text, print_tree, shorten, tab_tree_label, tree_title_limit, tree_url_limit
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
|
from rich.text import Text
|
||||||
from rich.tree import Tree
|
from rich.tree import Tree
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
def _group_tree_label(group_id: int, group, *, title_limit: int) -> Text:
|
||||||
|
title = getattr(group, "title", "") or f"Group {group_id}"
|
||||||
|
color = getattr(group, "color", "") or "group"
|
||||||
|
count = getattr(group, "tab_count", 0) or 0
|
||||||
|
collapsed = bool(getattr(group, "collapsed", False))
|
||||||
|
label = no_wrap_text()
|
||||||
|
label.append(shorten(title, title_limit), style="bold")
|
||||||
|
meta = [color]
|
||||||
|
if count:
|
||||||
|
meta.append(f"{count} tab" + ("" if count == 1 else "s"))
|
||||||
|
if collapsed:
|
||||||
|
meta.append("collapsed")
|
||||||
|
label.append(" (" + ", ".join(meta) + ")", style="dim")
|
||||||
|
return label
|
||||||
|
|
||||||
|
def _tab_sort_key(tab):
|
||||||
|
return (
|
||||||
|
tab.browser or "",
|
||||||
|
tab.window_id,
|
||||||
|
getattr(tab, "index", 0),
|
||||||
|
tab.group_id if tab.group_id is not None else -1,
|
||||||
|
tab.id,
|
||||||
|
)
|
||||||
|
|
||||||
def _print_tabs(tabs, *, show_browser: bool = False) -> None:
|
def _print_tabs(tabs, *, show_browser: bool = False) -> None:
|
||||||
if not tabs:
|
if not tabs:
|
||||||
console.print("[yellow]No tabs found[/yellow]")
|
console.print("[yellow]No tabs found[/yellow]")
|
||||||
@@ -48,10 +74,18 @@ def tabs_list():
|
|||||||
_print_tabs(tabs, show_browser=any(t.browser for t in tabs))
|
_print_tabs(tabs, show_browser=any(t.browser for t in tabs))
|
||||||
|
|
||||||
@tabs_group.command("tree")
|
@tabs_group.command("tree")
|
||||||
|
@click.option("--urls", "show_urls", is_flag=True, help="Show shortened URLs next to tab titles")
|
||||||
@handle_errors
|
@handle_errors
|
||||||
def tabs_tree():
|
def tabs_tree(show_urls):
|
||||||
"""Show tabs grouped as a window/group tree."""
|
"""Show tabs grouped as a window/group tree."""
|
||||||
tabs = sorted(client_from_ctx().tabs.list(), key=lambda t: ((t.browser or ""), t.window_id, t.group_id if t.group_id is not None else -1, t.index))
|
client = client_from_ctx()
|
||||||
|
tabs = sorted(client.tabs.list(), key=_tab_sort_key)
|
||||||
|
title_limit = tree_title_limit(console=console, show_browser=any(t.browser for t in tabs), show_urls=show_urls)
|
||||||
|
url_limit = tree_url_limit(title_limit, console=console)
|
||||||
|
group_info = {
|
||||||
|
(group.browser or "local", group.id): group
|
||||||
|
for group in client.groups.list()
|
||||||
|
}
|
||||||
root = Tree("[bold]Tabs[/bold]")
|
root = Tree("[bold]Tabs[/bold]")
|
||||||
browsers = {}
|
browsers = {}
|
||||||
windows = {}
|
windows = {}
|
||||||
@@ -59,21 +93,26 @@ def tabs_tree():
|
|||||||
show_browser = any(t.browser for t in tabs)
|
show_browser = any(t.browser for t in tabs)
|
||||||
for tab in tabs:
|
for tab in tabs:
|
||||||
browser_key = tab.browser or "local"
|
browser_key = tab.browser or "local"
|
||||||
browser_node = browsers.setdefault(browser_key, root.add(f"[bold cyan]{browser_key}[/bold cyan]") if show_browser else root)
|
browser_node = browsers.get(browser_key)
|
||||||
|
if browser_node is None:
|
||||||
|
browser_node = root.add(Text(browser_key, style="bold cyan")) if show_browser else root
|
||||||
|
browsers[browser_key] = browser_node
|
||||||
win_key = (browser_key, tab.window_id)
|
win_key = (browser_key, tab.window_id)
|
||||||
win_node = windows.get(win_key)
|
win_node = windows.get(win_key)
|
||||||
if win_node is None:
|
if win_node is None:
|
||||||
win_node = browser_node.add(f"Window {tab.window_id}")
|
win_node = browser_node.add(f"Window {tab.window_id}")
|
||||||
windows[win_key] = win_node
|
windows[win_key] = win_node
|
||||||
group_label = f"Group {tab.group_id}" if tab.group_id is not None else "Ungrouped"
|
if tab.group_id is None:
|
||||||
group_key = (browser_key, tab.window_id, group_label)
|
win_node.add(tab_tree_label(tab, title_limit=title_limit, show_urls=show_urls, url_limit=url_limit))
|
||||||
|
continue
|
||||||
|
group_key = (browser_key, tab.window_id, tab.group_id)
|
||||||
group_node = groups.get(group_key)
|
group_node = groups.get(group_key)
|
||||||
|
group = group_info.get((browser_key, tab.group_id))
|
||||||
if group_node is None:
|
if group_node is None:
|
||||||
group_node = win_node.add(group_label)
|
group_node = win_node.add(_group_tree_label(tab.group_id, group, title_limit=title_limit))
|
||||||
groups[group_key] = group_node
|
groups[group_key] = group_node
|
||||||
active = " [green]*[/green]" if tab.active else ""
|
group_node.add(tab_tree_label(tab, title_limit=title_limit, show_urls=show_urls, url_limit=url_limit))
|
||||||
group_node.add(f"[{tab.id}] {tab.title or '(untitled)'}{active} — [dim]{tab.url or ''}[/dim]")
|
print_tree(root, console=console)
|
||||||
console.print(root)
|
|
||||||
|
|
||||||
@tabs_group.command("close")
|
@tabs_group.command("close")
|
||||||
@click.argument("tab_id", type=int, required=False)
|
@click.argument("tab_id", type=int, required=False)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import click
|
import click
|
||||||
from browser_cli.commands import client_from_ctx, handle_errors
|
from browser_cli.commands import client_from_ctx, handle_errors
|
||||||
|
from browser_cli.commands.rendering import print_tree, tab_tree_label, tree_title_limit, tree_url_limit
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
from rich.tree import Tree
|
from rich.tree import Tree
|
||||||
@@ -47,6 +48,8 @@ def windows_tree():
|
|||||||
windows = client.windows.list()
|
windows = client.windows.list()
|
||||||
tabs = client.tabs.list()
|
tabs = client.tabs.list()
|
||||||
root = Tree("[bold]Windows[/bold]")
|
root = Tree("[bold]Windows[/bold]")
|
||||||
|
title_limit = tree_title_limit(console=console, show_browser=any("browser" in w for w in windows), show_urls=True)
|
||||||
|
url_limit = tree_url_limit(title_limit, console=console)
|
||||||
for w in sorted(windows, key=lambda item: (item.get("browser", ""), item.get("id", 0))):
|
for w in sorted(windows, key=lambda item: (item.get("browser", ""), item.get("id", 0))):
|
||||||
wid = w.get("id")
|
wid = w.get("id")
|
||||||
label = f"Window {wid}"
|
label = f"Window {wid}"
|
||||||
@@ -55,10 +58,9 @@ def windows_tree():
|
|||||||
if w.get("browser"):
|
if w.get("browser"):
|
||||||
label = f"{w['browser']}: " + label
|
label = f"{w['browser']}: " + label
|
||||||
node = root.add(label)
|
node = root.add(label)
|
||||||
for tab in sorted([t for t in tabs if t.window_id == wid and (not w.get("browser") or t.browser == w.get("browser"))], key=lambda t: t.index):
|
for tab in sorted([t for t in tabs if t.window_id == wid and (not w.get("browser") or t.browser == w.get("browser"))], key=lambda t: getattr(t, "index", 0)):
|
||||||
active = " [green]*[/green]" if tab.active else ""
|
node.add(tab_tree_label(tab, title_limit=title_limit, show_urls=True, url_limit=url_limit))
|
||||||
node.add(f"[{tab.id}] {tab.title or '(untitled)'}{active} — [dim]{tab.url or ''}[/dim]")
|
print_tree(root, console=console)
|
||||||
console.print(root)
|
|
||||||
|
|
||||||
@windows_group.command("rename")
|
@windows_group.command("rename")
|
||||||
@click.argument("window_id", type=int)
|
@click.argument("window_id", type=int)
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ class Tab:
|
|||||||
title: str = ""
|
title: str = ""
|
||||||
url: str = ""
|
url: str = ""
|
||||||
group_id: int | None = None
|
group_id: int | None = None
|
||||||
|
index: int = 0
|
||||||
browser: str | None = None
|
browser: str | None = None
|
||||||
_browser: BoundBrowser | None = field(default=None, repr=False, compare=False, init=False)
|
_browser: BoundBrowser | None = field(default=None, repr=False, compare=False, init=False)
|
||||||
|
|
||||||
@@ -149,6 +150,7 @@ class Group:
|
|||||||
color: str
|
color: str
|
||||||
collapsed: bool
|
collapsed: bool
|
||||||
tab_count: int
|
tab_count: int
|
||||||
|
window_id: int | None = None
|
||||||
browser: str | None = None
|
browser: str | None = None
|
||||||
_browser: BoundBrowser | None = field(default=None, repr=False, compare=False, init=False)
|
_browser: BoundBrowser | None = field(default=None, repr=False, compare=False, init=False)
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class FactoryMixin:
|
|||||||
title=data.get("title") or "",
|
title=data.get("title") or "",
|
||||||
url=data.get("url") or "",
|
url=data.get("url") or "",
|
||||||
group_id=data.get("groupId") or None,
|
group_id=data.get("groupId") or None,
|
||||||
|
index=data.get("index", 0) or 0,
|
||||||
browser=browser_name,
|
browser=browser_name,
|
||||||
)
|
)
|
||||||
client = cast(_FactoryClient, self)
|
client = cast(_FactoryClient, self)
|
||||||
@@ -68,6 +69,7 @@ class FactoryMixin:
|
|||||||
color=data.get("color") or "",
|
color=data.get("color") or "",
|
||||||
collapsed=data.get("collapsed", False),
|
collapsed=data.get("collapsed", False),
|
||||||
tab_count=data.get("tabCount", 0),
|
tab_count=data.get("tabCount", 0),
|
||||||
|
window_id=data.get("windowId"),
|
||||||
browser=browser_name,
|
browser=browser_name,
|
||||||
)
|
)
|
||||||
client = cast(_FactoryClient, self)
|
client = cast(_FactoryClient, self)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export function tabInfo(t: Tab) {
|
|||||||
windowId: t.windowId,
|
windowId: t.windowId,
|
||||||
active: t.active,
|
active: t.active,
|
||||||
muted: Boolean(t.mutedInfo && t.mutedInfo.muted),
|
muted: Boolean(t.mutedInfo && t.mutedInfo.muted),
|
||||||
|
index: t.index,
|
||||||
groupId: t.groupId >= 0 ? t.groupId : null,
|
groupId: t.groupId >= 0 ? t.groupId : null,
|
||||||
title: t.title,
|
title: t.title,
|
||||||
url: t.url || t.pendingUrl || "",
|
url: t.url || t.pendingUrl || "",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ TAB_DATA = {
|
|||||||
"id": 10,
|
"id": 10,
|
||||||
"windowId": 1,
|
"windowId": 1,
|
||||||
"active": True,
|
"active": True,
|
||||||
|
"index": 3,
|
||||||
"title": "Example",
|
"title": "Example",
|
||||||
"url": "https://example.com",
|
"url": "https://example.com",
|
||||||
"groupId": None,
|
"groupId": None,
|
||||||
@@ -73,6 +74,15 @@ class TestBrowserCLIInit:
|
|||||||
assert b.remote == "browser-host.example:443"
|
assert b.remote == "browser-host.example:443"
|
||||||
assert b.key == "agent"
|
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):
|
def test_namespaces_present_and_bound(self):
|
||||||
b = BrowserCLI()
|
b = BrowserCLI()
|
||||||
for name in ("nav", "tabs", "groups", "windows", "dom", "extract",
|
for name in ("nav", "tabs", "groups", "windows", "dom", "extract",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from click.testing import CliRunner
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from browser_cli import BrowserCLI
|
from browser_cli import BrowserCLI
|
||||||
|
from browser_cli.client import BrowserTarget
|
||||||
from browser_cli.cli import main
|
from browser_cli.cli import main
|
||||||
from browser_cli.command_security import CommandPolicy, assert_command_allowed, command_category
|
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"}),
|
("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():
|
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"])
|
result = CliRunner().invoke(main, ["tabs", "tree"])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "Tabs" in result.output
|
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():
|
def test_doctor_command_reports_connection_failure_cleanly():
|
||||||
with patch("browser_cli.commands.doctor.active_browser_targets", return_value=[]), \
|
with patch("browser_cli.commands.doctor.active_browser_targets", return_value=[]), \
|
||||||
patch("browser_cli.send_command", side_effect=RuntimeError("no browser")):
|
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