add multi browser mode to arragate data from all browsers by tabs list, tabs count, group list, group count and windows list
remove (unnamed) into the group names just leave it a empty string, remove Focused on windows how should the browser know what windows are focused
This commit is contained in:
+104
-3
@@ -5,8 +5,8 @@ These tests mock `send_command` so no live browser connection is required.
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch, call
|
||||
|
||||
from browser_cli import BrowserCLI, Tab, Group
|
||||
from browser_cli.client import BrowserNotConnected
|
||||
from browser_cli import BrowserCLI, BrowserCounts, Tab, Group
|
||||
from browser_cli.client import BrowserNotConnected, BrowserTarget
|
||||
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
@@ -36,7 +36,7 @@ def mock_send():
|
||||
BrowserCLI._cmd calls the `send_command` name that was imported into
|
||||
browser_cli/__init__.py, so we must patch it there, not in the client module.
|
||||
"""
|
||||
with patch("browser_cli.send_command") as m:
|
||||
with patch("browser_cli.send_command") as m, patch("browser_cli.active_browser_targets", return_value=[]):
|
||||
yield m
|
||||
|
||||
|
||||
@@ -268,6 +268,48 @@ class TestTabs:
|
||||
mock_send.return_value = 5
|
||||
assert b.tabs_count() == 5
|
||||
|
||||
def test_tabs_list_multi_browser_annotates_browser_and_binds_actions(self, b, mock_send):
|
||||
with patch(
|
||||
"browser_cli.active_browser_targets",
|
||||
return_value=[
|
||||
BrowserTarget("default", "uuid-1", "/tmp/uuid-1.sock"),
|
||||
BrowserTarget("work", "work", "/tmp/work.sock"),
|
||||
],
|
||||
):
|
||||
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] == ["uuid-1", "work"]
|
||||
assert [tab.id for tab in tabs] == [10, 11]
|
||||
assert mock_send.call_args_list == [
|
||||
call("tabs.list", {}, profile="default"),
|
||||
call("tabs.list", {}, profile="work"),
|
||||
call("tabs.close", {"tabId": 11}, profile="work"),
|
||||
]
|
||||
|
||||
def test_tabs_count_multi_browser_returns_browser_counts(self, b, mock_send):
|
||||
with patch(
|
||||
"browser_cli.active_browser_targets",
|
||||
return_value=[
|
||||
BrowserTarget("default", "uuid-1", "/tmp/uuid-1.sock"),
|
||||
BrowserTarget("work", "work", "/tmp/work.sock"),
|
||||
],
|
||||
):
|
||||
mock_send.side_effect = [3, 4]
|
||||
result = b.tabs_count("github")
|
||||
|
||||
assert result == BrowserCounts(total=7, by_browser={"uuid-1": 3, "work": 4})
|
||||
assert mock_send.call_args_list == [
|
||||
call("tabs.count", {"pattern": "github"}, profile="default"),
|
||||
call("tabs.count", {"pattern": "github"}, profile="work"),
|
||||
]
|
||||
|
||||
def test_tabs_query(self, b, mock_send):
|
||||
mock_send.return_value = [TAB_DATA]
|
||||
result = b.tabs_query("example")
|
||||
@@ -330,6 +372,44 @@ class TestGroups:
|
||||
mock_send.return_value = 7
|
||||
assert b.group_count() == 7
|
||||
|
||||
def test_group_list_multi_browser_annotates_browser_and_binds_actions(self, b, mock_send):
|
||||
with patch(
|
||||
"browser_cli.active_browser_targets",
|
||||
return_value=[
|
||||
BrowserTarget("default", "uuid-1", "/tmp/uuid-1.sock"),
|
||||
BrowserTarget("work", "work", "/tmp/work.sock"),
|
||||
],
|
||||
):
|
||||
mock_send.side_effect = [
|
||||
[GROUP_DATA],
|
||||
[{**GROUP_DATA, "id": 99, "title": "Later"}],
|
||||
None,
|
||||
]
|
||||
|
||||
groups = b.group_list()
|
||||
groups[1].close()
|
||||
|
||||
assert [group.browser for group in groups] == ["uuid-1", "work"]
|
||||
assert [group.id for group in groups] == [42, 99]
|
||||
assert mock_send.call_args_list == [
|
||||
call("group.list", {}, profile="default"),
|
||||
call("group.list", {}, profile="work"),
|
||||
call("group.close", {"groupId": 99}, profile="work"),
|
||||
]
|
||||
|
||||
def test_group_count_multi_browser_returns_browser_counts(self, b, mock_send):
|
||||
with patch(
|
||||
"browser_cli.active_browser_targets",
|
||||
return_value=[
|
||||
BrowserTarget("default", "uuid-1", "/tmp/uuid-1.sock"),
|
||||
BrowserTarget("work", "work", "/tmp/work.sock"),
|
||||
],
|
||||
):
|
||||
mock_send.side_effect = [2, 5]
|
||||
result = b.group_count()
|
||||
|
||||
assert result == BrowserCounts(total=7, by_browser={"uuid-1": 2, "work": 5})
|
||||
|
||||
def test_group_query(self, b, mock_send):
|
||||
mock_send.return_value = [GROUP_DATA]
|
||||
groups = b.group_query("Work")
|
||||
@@ -371,6 +451,27 @@ class TestGroups:
|
||||
)
|
||||
|
||||
|
||||
class TestWindows:
|
||||
def test_windows_list_multi_browser_adds_browser(self, b, mock_send):
|
||||
with patch(
|
||||
"browser_cli.active_browser_targets",
|
||||
return_value=[
|
||||
BrowserTarget("default", "uuid-1", "/tmp/uuid-1.sock"),
|
||||
BrowserTarget("work", "work", "/tmp/work.sock"),
|
||||
],
|
||||
):
|
||||
mock_send.side_effect = [
|
||||
[{"id": 1, "tabCount": 2, "state": "normal"}],
|
||||
[{"id": 2, "tabCount": 3, "state": "maximized"}],
|
||||
]
|
||||
result = b.windows_list()
|
||||
|
||||
assert result == [
|
||||
{"id": 1, "tabCount": 2, "state": "normal", "browser": "uuid-1"},
|
||||
{"id": 2, "tabCount": 3, "state": "maximized", "browser": "work"},
|
||||
]
|
||||
|
||||
|
||||
# ── Tab model ─────────────────────────────────────────────────────────────────
|
||||
|
||||
class TestTabModel:
|
||||
|
||||
+120
-2
@@ -4,6 +4,7 @@ from click.testing import CliRunner
|
||||
from unittest.mock import patch
|
||||
|
||||
from browser_cli.cli import main, _project_version
|
||||
from browser_cli.client import BrowserTarget
|
||||
|
||||
def _expected_version() -> str:
|
||||
pyproject = Path(__file__).resolve().parent.parent / "pyproject.toml"
|
||||
@@ -50,7 +51,7 @@ def test_install_help_lists_supported_browsers():
|
||||
assert "[chrome|chromium|brave|edge|vivaldi]" in result.output
|
||||
|
||||
def test_clients_exits_cleanly_when_registry_is_missing():
|
||||
with patch("browser_cli.client.REGISTRY_PATH", Path("/nonexistent/browser-cli-registry.json")):
|
||||
with patch("browser_cli.cli.REGISTRY_PATH", Path("/nonexistent/browser-cli-registry.json")):
|
||||
result = CliRunner().invoke(main, ["clients"])
|
||||
|
||||
assert result.exit_code == 1
|
||||
@@ -74,7 +75,7 @@ def test_clients_shows_named_profile_and_uses_socket_uuid_for_default(tmp_path):
|
||||
assert command == "clients.list"
|
||||
return responses[profile]
|
||||
|
||||
with patch("browser_cli.client.REGISTRY_PATH", registry_path), patch(
|
||||
with patch("browser_cli.cli.REGISTRY_PATH", registry_path), patch(
|
||||
"browser_cli.cli.send_command", side_effect=fake_send_command
|
||||
):
|
||||
result = CliRunner().invoke(main, ["clients"])
|
||||
@@ -85,6 +86,123 @@ def test_clients_shows_named_profile_and_uses_socket_uuid_for_default(tmp_path):
|
||||
assert "Extension Version" in result.output
|
||||
assert "2.3.4" in result.output
|
||||
|
||||
|
||||
def test_tabs_list_multi_browser_shows_browser_column():
|
||||
def fake_send_command(command, args=None, profile=None):
|
||||
assert command == "tabs.list"
|
||||
return [{"id": 1 if profile == "default" else 2, "windowId": 1, "active": True, "title": profile, "url": "https://example.com"}]
|
||||
|
||||
with patch(
|
||||
"browser_cli.commands.tabs.active_browser_targets",
|
||||
return_value=[
|
||||
BrowserTarget("default", "550e8400-e29b-41d4-a716-446655440000", "/tmp/default.sock"),
|
||||
BrowserTarget("work", "work", "/tmp/work.sock"),
|
||||
],
|
||||
), patch("browser_cli.commands.tabs.send_command", side_effect=fake_send_command):
|
||||
result = CliRunner().invoke(main, ["tabs", "list"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Browser" in result.output
|
||||
assert "550e8400-e29b-41d4-a716-446655440000" in result.output
|
||||
assert "work" in result.output
|
||||
|
||||
|
||||
def test_tabs_list_with_explicit_browser_does_not_show_browser_column():
|
||||
with patch(
|
||||
"browser_cli.commands.tabs.active_browser_targets",
|
||||
return_value=[
|
||||
BrowserTarget("default", "uuid-1", "/tmp/default.sock"),
|
||||
BrowserTarget("work", "work", "/tmp/work.sock"),
|
||||
],
|
||||
), patch(
|
||||
"browser_cli.commands.tabs.send_command",
|
||||
return_value=[{"id": 1, "windowId": 1, "active": True, "title": "Example", "url": "https://example.com"}],
|
||||
) as send_command:
|
||||
result = CliRunner().invoke(main, ["--browser", "work", "tabs", "list"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Browser" not in result.output
|
||||
send_command.assert_called_once_with("tabs.list", {}, profile=None)
|
||||
|
||||
|
||||
def test_tabs_count_multi_browser_shows_total():
|
||||
counts = {"default": 3, "work": 4}
|
||||
|
||||
def fake_send_command(command, args=None, profile=None):
|
||||
assert command == "tabs.count"
|
||||
assert args == {"pattern": "github"}
|
||||
return counts[profile]
|
||||
|
||||
with patch(
|
||||
"browser_cli.commands.tabs.active_browser_targets",
|
||||
return_value=[
|
||||
BrowserTarget("default", "uuid-1", "/tmp/default.sock"),
|
||||
BrowserTarget("work", "work", "/tmp/work.sock"),
|
||||
],
|
||||
), patch("browser_cli.commands.tabs.send_command", side_effect=fake_send_command):
|
||||
result = CliRunner().invoke(main, ["tabs", "count", "github"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Browser" in result.output
|
||||
assert "Total" in result.output
|
||||
assert "7" in result.output
|
||||
|
||||
|
||||
def test_group_count_multi_browser_shows_total():
|
||||
counts = {"default": 1, "work": 2}
|
||||
|
||||
def fake_send_command(command, args=None, profile=None):
|
||||
assert command == "group.count"
|
||||
return counts[profile]
|
||||
|
||||
with patch(
|
||||
"browser_cli.commands.groups.active_browser_targets",
|
||||
return_value=[
|
||||
BrowserTarget("default", "uuid-1", "/tmp/default.sock"),
|
||||
BrowserTarget("work", "work", "/tmp/work.sock"),
|
||||
],
|
||||
), patch("browser_cli.commands.groups.send_command", side_effect=fake_send_command):
|
||||
result = CliRunner().invoke(main, ["group", "count"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Browser" in result.output
|
||||
assert "Total" in result.output
|
||||
assert "3" in result.output
|
||||
|
||||
|
||||
def test_group_list_leaves_unnamed_group_cell_empty():
|
||||
with patch(
|
||||
"browser_cli.commands.groups.send_command",
|
||||
return_value=[{"id": 42, "title": "", "color": "grey", "collapsed": False, "tabCount": 1}],
|
||||
):
|
||||
result = CliRunner().invoke(main, ["group", "list"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "(unnamed)" not in result.output
|
||||
assert "42" in result.output
|
||||
assert "grey" in result.output
|
||||
|
||||
|
||||
def test_windows_list_multi_browser_shows_browser_column():
|
||||
def fake_send_command(command, args=None, profile=None):
|
||||
assert command == "windows.list"
|
||||
return [{"id": 1, "alias": profile, "focused": True, "tabCount": 2, "state": "normal"}]
|
||||
|
||||
with patch(
|
||||
"browser_cli.commands.windows.active_browser_targets",
|
||||
return_value=[
|
||||
BrowserTarget("default", "uuid-1", "/tmp/default.sock"),
|
||||
BrowserTarget("work", "work", "/tmp/work.sock"),
|
||||
],
|
||||
), patch("browser_cli.commands.windows.send_command", side_effect=fake_send_command):
|
||||
result = CliRunner().invoke(main, ["windows", "list"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Browser" in result.output
|
||||
assert "Focused" not in result.output
|
||||
assert "uuid-1" in result.output
|
||||
assert "work" in result.output
|
||||
|
||||
def test_extract_markdown_command():
|
||||
with patch("browser_cli.commands.extract.send_command", return_value="# Title\n") as send_command:
|
||||
result = CliRunner().invoke(main, ["extract", "markdown"])
|
||||
|
||||
+23
-1
@@ -3,7 +3,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from browser_cli.client import BrowserNotConnected, _resolve_socket
|
||||
from browser_cli.client import BrowserNotConnected, _resolve_socket, active_browser_targets, display_browser_name
|
||||
|
||||
def test_resolve_socket_raises_when_registry_missing(monkeypatch):
|
||||
monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False)
|
||||
@@ -38,3 +38,25 @@ def test_resolve_socket_raises_when_multiple_active_entries(monkeypatch, tmp_pat
|
||||
|
||||
with pytest.raises(BrowserNotConnected, match="Multiple browser instances are active: uuid-1, uuid-2"):
|
||||
_resolve_socket()
|
||||
|
||||
|
||||
def test_display_browser_name_uses_uuid_stem_for_default():
|
||||
assert display_browser_name("default", "/tmp/.browser_cli/550e8400-e29b-41d4-a716-446655440000.sock") == (
|
||||
"550e8400-e29b-41d4-a716-446655440000"
|
||||
)
|
||||
|
||||
|
||||
def test_active_browser_targets_filters_stale_entries(monkeypatch, tmp_path):
|
||||
active_socket = tmp_path / "work.sock"
|
||||
active_socket.write_text("")
|
||||
stale_socket = tmp_path / "stale.sock"
|
||||
registry_path = tmp_path / "registry.json"
|
||||
registry_path.write_text(json.dumps({"work": str(active_socket), "default": str(stale_socket)}))
|
||||
|
||||
monkeypatch.setattr("browser_cli.client.REGISTRY_PATH", registry_path)
|
||||
|
||||
targets = active_browser_targets()
|
||||
|
||||
assert len(targets) == 1
|
||||
assert targets[0].profile == "work"
|
||||
assert targets[0].display_name == "work"
|
||||
|
||||
Reference in New Issue
Block a user