feat: improve remote browser tree routing
Testing / remote-protocol-compat (0.9.3) (push) Successful in 43s
Testing / test (push) Successful in 1m1s
Testing / remote-protocol-compat (0.9.5) (push) Successful in 39s
Build & Publish Package / publish (push) Successful in 58s
Package Extension / package-extension (push) Successful in 1m15s

- Allow remote host aliases passed via --browser to fan out for read-only
  multi-browser SDK paths while preserving strict routing for mutating commands.
- Add remote host grouping and scoped profile labels to tabs tree output so
  global views avoid repeated host prefixes.
- Carry browser family metadata through remote targets, tabs, and groups and
  style tree browser labels by family.
- Split CLI rendering helpers into a typed rendering package with dedicated
  common, label, tabs-tree, and windows-tree modules.
- Bump browser-cli and extension versions to 0.15.5.
- Cover the new routing and rendering behavior with unit and CLI tests.
This commit is contained in:
2026-06-18 00:12:17 +02:00
parent 371b794170
commit 479a0f1964
22 changed files with 672 additions and 221 deletions
+24 -2
View File
@@ -470,7 +470,7 @@ class TestTabs:
tabs = b.tabs.list()
tabs[0].close()
assert [tab.browser for tab in tabs] == ["host:work"]
assert [tab.browser for tab in tabs] == ["work"]
assert mock_send.call_args_list == [
call("tabs.list", {}, profile="work", remote="host:8765", key=None),
call("tabs.close", {"tabId": 10}, profile="work", remote="host:8765", key=None),
@@ -491,6 +491,28 @@ class TestTabs:
call("tabs.close", {"tabId": 10}, profile="work", remote="browser-host.example", key="agent"),
]
def test_tabs_list_browser_host_alias_fans_out_to_remote_targets(self, mock_send):
b = BrowserCLI(browser="browser-host.example", key="agent")
with patch(
"browser_cli.remote_targets_for_alias",
return_value=[
BrowserTarget("main", "browser-host.example:main", "", remote="browser-host.example:8765", browser_name="Chrome"),
BrowserTarget("work", "browser-host.example:work", "", remote="browser-host.example:8765", browser_name="Firefox"),
],
):
mock_send.side_effect = [[TAB_DATA], [{**TAB_DATA, "id": 11}], None]
tabs = b.tabs.list()
tabs[1].close()
assert [tab.browser for tab in tabs] == ["main", "work"]
assert [tab.browser_name for tab in tabs] == ["Chrome", "Firefox"]
assert [tab.browser_group for tab in tabs] == [None, None]
assert mock_send.call_args_list == [
call("tabs.list", {}, profile="main", remote="browser-host.example:8765", key="agent"),
call("tabs.list", {}, profile="work", remote="browser-host.example:8765", key="agent"),
call("tabs.close", {"tabId": 11}, profile="work", remote="browser-host.example:8765", key="agent"),
]
def test_tabs_active_returns_active_tab(self, b, mock_send):
mock_send.side_effect = [[TAB_DATA], TAB_DATA]
@@ -659,7 +681,7 @@ class TestGroups:
groups = b.groups.list()
groups[0].close()
assert [group.browser for group in groups] == ["host:work"]
assert [group.browser for group in groups] == ["work"]
assert mock_send.call_args_list == [
call("group.list", {}, profile="work", remote="host:8765", key=None),
call("group.close", {"groupId": 42}, profile="work", remote="host:8765", key=None),
+78 -2
View File
@@ -4,7 +4,7 @@ import os
import sys
from click.testing import CliRunner
from unittest.mock import patch
from unittest.mock import call, patch
from browser_cli.cli import main, _project_version
from browser_cli.client import BrowserTarget
@@ -379,7 +379,8 @@ def test_tabs_list_with_remote_uses_only_remote_targets():
result = CliRunner().invoke(main, ["--remote", "remote-host:8765", "tabs", "list"])
assert result.exit_code == 0
assert "remote-host:work" in result.output
assert "work" in result.output
assert "remote-host:work" not in result.output
assert "Remote" in result.output
send_command.assert_called_once_with("tabs.list", {}, profile="work", remote="remote-host:8765", key=None)
@@ -400,6 +401,81 @@ def test_tabs_list_with_explicit_browser_does_not_show_browser_column():
assert "Browser" not in result.output
send_command.assert_called_once_with("tabs.list", {}, profile="work", remote=None, key=None)
def test_tabs_tree_with_browser_host_alias_fans_out_to_remote_targets():
targets = [
BrowserTarget("main", "browser-host.example:main", "", remote="browser-host.example:8765", browser_name="Chrome"),
BrowserTarget("work", "browser-host.example:work", "", remote="browser-host.example:8765", browser_name="Firefox"),
]
def fake_send_command(command, args=None, profile=None, remote=None, key=None):
assert remote == "browser-host.example:8765"
if command == "tabs.list":
return [{
"id": 1 if profile == "main" else 2,
"windowId": 1,
"active": True,
"index": 0,
"title": f"{profile} tab",
"url": "https://example.com",
}]
if command == "group.list":
return []
raise AssertionError(command)
with patch("browser_cli.remote_targets_for_alias", return_value=targets), patch(
"browser_cli.send_command", side_effect=fake_send_command
) as send_command:
result = CliRunner().invoke(main, ["--browser", "browser-host.example", "tabs", "tree"])
assert result.exit_code == 0
assert "main" in result.output
assert "work" in result.output
assert "browser-host.example:main" not in result.output
assert "browser-host.example:work" not in result.output
assert "main tab" in result.output
assert "work tab" in result.output
assert send_command.call_args_list == [
call("tabs.list", {}, profile="main", remote="browser-host.example:8765", key=None),
call("tabs.list", {}, profile="work", remote="browser-host.example:8765", key=None),
call("group.list", {}, profile="main", remote="browser-host.example:8765", key=None),
call("group.list", {}, profile="work", remote="browser-host.example:8765", key=None),
]
def test_tabs_tree_unscoped_groups_remote_targets_by_host():
targets = [
BrowserTarget("main", "browser-host.example:main", "", remote="browser-host.example:8765", display_group="browser-host.example"),
BrowserTarget("work", "browser-host.example:work", "", remote="browser-host.example:8765", display_group="browser-host.example"),
]
def fake_send_command(command, args=None, profile=None, remote=None, key=None):
assert remote == "browser-host.example:8765"
if command == "tabs.list":
return [{
"id": 1 if profile == "main" else 2,
"windowId": 1,
"active": True,
"index": 0,
"title": f"{profile} tab",
"url": "https://example.com",
}]
if command == "group.list":
return []
raise AssertionError(command)
with patch("browser_cli.active_browser_targets", return_value=targets), patch(
"browser_cli.send_command", side_effect=fake_send_command
):
result = CliRunner().invoke(main, ["tabs", "tree"])
assert result.exit_code == 0
assert "browser-host.example" in result.output
assert "main" in result.output
assert "work" in result.output
assert "browser-host.example:main" not in result.output
assert "browser-host.example:work" not in result.output
assert "main tab" in result.output
assert "work tab" in result.output
def test_tabs_count_multi_browser_shows_total():
counts = {"default": 3, "work": 4}
+3 -1
View File
@@ -342,7 +342,7 @@ def test_active_browser_targets_includes_remote_targets(monkeypatch, tmp_path):
def fake_send_command(command, args=None, profile=None, remote=None, key=None):
assert command == "browser-cli.targets"
assert remote == endpoint
return [{"profile": "work", "displayName": "work"}]
return [{"profile": "work", "displayName": "work", "browserName": "Firefox"}]
monkeypatch.setattr("browser_cli.client.core.send_command", fake_send_command)
@@ -352,6 +352,8 @@ def test_active_browser_targets_includes_remote_targets(monkeypatch, tmp_path):
assert targets[0].profile == "work"
assert targets[0].display_name == "browser-host.example:work"
assert targets[0].remote == endpoint
assert targets[0].browser_name == "Firefox"
assert targets[0].display_group == "browser-host.example"
def test_looks_like_domain():
assert _looks_like_domain("browsercli.yiprawr.dev") is True
+29 -3
View File
@@ -5,15 +5,26 @@ from rich.tree import Tree
from browser_cli.models import Tab
from browser_cli.commands import rendering
from browser_cli.commands.rendering import common
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)))
monkeypatch.setattr(common.shutil, "get_terminal_size", lambda fallback: terminal_size((140, 20)))
assert rendering.terminal_width(Console(width=80)) == 140
def test_browser_label_style_distinguishes_browser_families():
assert rendering.browser_label_style("Firefox") == "orange1"
assert rendering.browser_label_style("Chrome") == "cyan"
assert rendering.browser_label_style(None) == "bold cyan"
def test_scoped_browser_label_strips_repeated_remote_prefix():
assert rendering.scoped_browser_label("browser-host.example:work", "browser-host.example", grouped=True) == "work"
assert rendering.scoped_browser_label("work", "browser-host.example", grouped=True) == "work"
assert rendering.scoped_browser_label("browser-host.example:work", "browser-host.example", grouped=False) == "browser-host.example:work"
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)
@@ -29,8 +40,8 @@ def test_print_tree_uses_detected_width(monkeypatch):
widths.append(kwargs.get("width"))
super().__init__(*args, **kwargs)
monkeypatch.setattr(rendering, "Console", CapturingConsole)
monkeypatch.setattr(rendering, "terminal_width", lambda console=None: 132)
monkeypatch.setattr(common, "Console", CapturingConsole)
monkeypatch.setattr(common, "terminal_width", lambda console=None: 132)
rendering.print_tree(Tree("Root"))
assert widths == [132]
@@ -48,6 +59,21 @@ def test_build_tabs_tree_groups_by_browser_window_and_group():
assert "collapsed" in text
assert "Inside" in text
def test_build_tabs_tree_groups_remote_browsers_by_scope():
tabs = [
Tab(id=1, window_id=5, active=False, muted=False, title="Remote A", url="https://example.com/a", index=0, browser="main", browser_group="browser-host.example"),
Tab(id=2, window_id=6, active=False, muted=False, title="Remote B", url="https://example.com/b", index=0, browser="work", browser_group="browser-host.example"),
Tab(id=3, window_id=7, active=False, muted=False, title="Local", url="https://example.com/local", index=0, browser="local"),
]
tree = rendering.build_tabs_tree(tabs, [], console=Console(width=120))
text = "\n".join(str(line) for line in tree.__rich_console__(Console(width=120), Console(width=120).options))
assert "browser-host.example" in text
assert "main" in text
assert "work" in text
assert "browser-host.example:main" not in text
assert "browser-host.example:work" not in text
assert "Local" in text
def test_build_windows_tree_keeps_multi_browser_windows_separate():
tabs = [
Tab(id=1, window_id=5, active=False, muted=False, title="Work Tab", url="https://example.com/work", index=0, browser="work"),