test: expand browser command coverage

- Add mocked Click CLI tests across DOM, cookies, page, storage, perf, navigation, tabs, groups, sessions, and windows commands.
- Cover integration paths for cookies, page info, performance profiles, and web storage commands.
- Extend DOM integration coverage for eval, scrolling, waiting, focus, hover, typing, selection, keyboard, and checkbox interactions.
- Add native host unit coverage for message framing, socket helpers, paging guards, timeouts, and profile alias resolution.
This commit is contained in:
2026-05-20 23:53:56 +02:00
parent 545abeb515
commit 9aad012bdc
7 changed files with 1689 additions and 9 deletions
+993
View File
@@ -0,0 +1,993 @@
"""
Unit tests for CLI command modules.
All send_command calls are mocked — no live browser required.
Uses Click's CliRunner to exercise the CLI output formatting paths.
"""
from unittest.mock import patch, MagicMock
import pytest
from click.testing import CliRunner
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _run(group, args, return_value):
"""Invoke a Click group/command with a mocked send_command return value."""
with patch("browser_cli.commands.send_command", return_value=return_value):
result = CliRunner().invoke(group, args)
return result
def _run_error(group, args, exc):
"""Invoke a Click group/command where send_command raises exc."""
with patch("browser_cli.commands.send_command", side_effect=exc):
result = CliRunner().invoke(group, args)
return result
def _run_no_multi(group, args, return_value, module_path=None):
"""Invoke a command patching send_command AND _multi_browser_targets → single-browser mode."""
ctx1 = patch("browser_cli.commands.send_command", return_value=return_value)
if module_path:
ctx2 = patch(f"{module_path}._multi_browser_targets", return_value=[])
with ctx1, ctx2:
return CliRunner().invoke(group, args)
else:
with ctx1:
return CliRunner().invoke(group, args)
# ---------------------------------------------------------------------------
# dom commands
# ---------------------------------------------------------------------------
from browser_cli.commands.dom import dom_group
def test_cli_dom_query_no_results():
result = _run(dom_group, ["query", "span.nothing"], [])
assert result.exit_code == 0
assert "No elements found" in result.output
def test_cli_dom_query_with_results():
elements = [{"tag": "div", "text": "hello", "attrs": {"class": "foo"}}]
result = _run(dom_group, ["query", "div"], elements)
assert result.exit_code == 0
assert "div" in result.output
def test_cli_dom_click():
result = _run(dom_group, ["click", "button#submit"], None)
assert result.exit_code == 0
assert "Clicked" in result.output
def test_cli_dom_type():
result = _run(dom_group, ["type", "input#name", "pytest"], None)
assert result.exit_code == 0
assert "Typed into" in result.output
def test_cli_dom_attr():
result = _run(dom_group, ["attr", "a", "href"], ["https://example.com"])
assert result.exit_code == 0
assert "https://example.com" in result.output
def test_cli_dom_attr_empty():
result = _run(dom_group, ["attr", "span", "href"], [])
assert result.exit_code == 0
def test_cli_dom_text():
result = _run(dom_group, ["text", "h1"], ["Example Domain"])
assert result.exit_code == 0
assert "Example Domain" in result.output
def test_cli_dom_text_empty():
result = _run(dom_group, ["text", "h9"], [])
assert result.exit_code == 0
def test_cli_dom_exists_true():
result = _run(dom_group, ["exists", "html"], True)
assert result.exit_code == 0
assert "exists" in result.output
def test_cli_dom_exists_false():
result = _run(dom_group, ["exists", "#no-such"], False)
assert result.exit_code != 0
assert "not found" in result.output
def test_cli_dom_scroll_coord():
result = _run(dom_group, ["scroll", "--x", "0", "--y", "500"], None)
assert result.exit_code == 0
assert "Scrolled" in result.output
def test_cli_dom_scroll_selector():
result = _run(dom_group, ["scroll", "footer"], None)
assert result.exit_code == 0
assert "Scrolled" in result.output
def test_cli_dom_eval_string():
result = _run(dom_group, ["eval", "document.title"], "My Page")
assert result.exit_code == 0
assert "My Page" in result.output
def test_cli_dom_eval_null():
result = _run(dom_group, ["eval", "void 0"], None)
assert result.exit_code == 0
assert "null" in result.output
def test_cli_dom_eval_dict():
result = _run(dom_group, ["eval", "({a:1})"], {"a": 1})
assert result.exit_code == 0
assert '"a"' in result.output
def test_cli_dom_wait_for():
result = _run(dom_group, ["wait-for", "html"], None)
assert result.exit_code == 0
assert "Ready" in result.output
def test_cli_dom_wait_for_visible():
result = _run(dom_group, ["wait-for", "html", "--visible"], None)
assert result.exit_code == 0
assert "visible" in result.output
def test_cli_dom_wait_for_hidden():
result = _run(dom_group, ["wait-for", "#spinner", "--hidden"], None)
assert result.exit_code == 0
assert "hidden" in result.output
def test_cli_dom_key():
result = _run(dom_group, ["key", "Enter"], None)
assert result.exit_code == 0
assert "Key 'Enter'" in result.output
def test_cli_dom_key_with_selector():
result = _run(dom_group, ["key", "Tab", "--selector", "input"], None)
assert result.exit_code == 0
assert "input" in result.output
def test_cli_dom_hover():
result = _run(dom_group, ["hover", ".menu"], None)
assert result.exit_code == 0
assert "Hovered" in result.output
def test_cli_dom_check():
result = _run(dom_group, ["check", "#accept"], None)
assert result.exit_code == 0
assert "Checked" in result.output
def test_cli_dom_uncheck():
result = _run(dom_group, ["uncheck", "#accept"], None)
assert result.exit_code == 0
assert "Unchecked" in result.output
def test_cli_dom_clear():
result = _run(dom_group, ["clear", "input#q"], None)
assert result.exit_code == 0
assert "Cleared" in result.output
def test_cli_dom_focus():
result = _run(dom_group, ["focus", "input#q"], None)
assert result.exit_code == 0
assert "Focused" in result.output
def test_cli_dom_submit():
result = _run(dom_group, ["submit", "form"], None)
assert result.exit_code == 0
assert "Submitted" in result.output
def test_cli_dom_select():
result = _run(dom_group, ["select", "#lang", "en"], None)
assert result.exit_code == 0
assert "Selected" in result.output
def test_cli_dom_poll():
result = _run(dom_group, ["poll", "#status", "ready"], {"value": "ready"})
assert result.exit_code == 0
assert "Matched" in result.output
# ---------------------------------------------------------------------------
# cookies commands
# ---------------------------------------------------------------------------
from browser_cli.commands.cookies import cookies_group
def test_cli_cookies_list_empty():
result = _run(cookies_group, ["list"], [])
assert result.exit_code == 0
assert "No cookies found" in result.output
def test_cli_cookies_list_with_cookies():
cookies = [{"name": "session", "value": "abc123", "domain": "example.com",
"path": "/", "secure": True, "httpOnly": False}]
result = _run(cookies_group, ["list"], cookies)
assert result.exit_code == 0
assert "session" in result.output
assert "example.com" in result.output
def test_cli_cookies_list_filter_url():
cookies = [{"name": "x", "value": "y", "domain": "example.com",
"path": "/", "secure": False, "httpOnly": False}]
result = _run(cookies_group, ["list", "--url", "https://example.com"], cookies)
assert result.exit_code == 0
assert "example.com" in result.output
def test_cli_cookies_list_filter_domain():
cookies = [{"name": "x", "value": "y", "domain": "example.com",
"path": "/", "secure": False, "httpOnly": False}]
result = _run(cookies_group, ["list", "--domain", "example.com"], cookies)
assert result.exit_code == 0
def test_cli_cookies_list_filter_name():
cookies = [{"name": "session", "value": "abc", "domain": "example.com",
"path": "/", "secure": False, "httpOnly": True}]
result = _run(cookies_group, ["list", "--name", "session"], cookies)
assert result.exit_code == 0
assert "session" in result.output
def test_cli_cookies_get_found():
cookie = {"name": "tok", "value": "secret123", "domain": "example.com", "path": "/"}
result = _run(cookies_group, ["get", "https://example.com", "tok"], cookie)
assert result.exit_code == 0
assert "secret123" in result.output
def test_cli_cookies_get_not_found():
result = _run(cookies_group, ["get", "https://example.com", "missing"], None)
assert result.exit_code != 0
assert "not found" in result.output
def test_cli_cookies_set():
result = _run(cookies_group, ["set", "https://example.com", "tok", "val"], None)
assert result.exit_code == 0
assert "Set cookie" in result.output
def test_cli_cookies_set_with_options():
result = _run(cookies_group, [
"set", "https://example.com", "tok", "val",
"--secure", "--http-only", "--path", "/app",
"--same-site", "lax",
], None)
assert result.exit_code == 0
assert "Set cookie" in result.output
# ---------------------------------------------------------------------------
# page commands
# ---------------------------------------------------------------------------
from browser_cli.commands.page import page_group
def test_cli_page_info_basic():
info = {"title": "Example", "url": "https://example.com", "readyState": "complete", "lang": "en", "meta": {}}
result = _run(page_group, ["info"], info)
assert result.exit_code == 0
assert "Example" in result.output
assert "https://example.com" in result.output
def test_cli_page_info_with_meta():
info = {"title": "My Page", "url": "https://example.com", "readyState": "complete", "lang": "de",
"meta": {"description": "A test page", "author": "Tester"}}
result = _run(page_group, ["info"], info)
assert result.exit_code == 0
assert "description" in result.output
assert "A test page" in result.output
def test_cli_page_info_empty_response():
result = _run(page_group, ["info"], {})
assert result.exit_code == 0
# ---------------------------------------------------------------------------
# storage commands
# ---------------------------------------------------------------------------
from browser_cli.commands.storage import storage_group
def test_cli_storage_get_null():
result = _run(storage_group, ["get", "mykey"], None)
assert result.exit_code == 0
assert "null" in result.output
def test_cli_storage_get_string():
result = _run(storage_group, ["get", "mykey"], "hello-world")
assert result.exit_code == 0
assert "hello-world" in result.output
def test_cli_storage_get_dict():
result = _run(storage_group, ["get"], {"k1": "v1", "k2": "v2"})
assert result.exit_code == 0
assert "k1" in result.output
def test_cli_storage_get_session():
result = _run(storage_group, ["get", "key", "--type", "session"], "ses-val")
assert result.exit_code == 0
assert "ses-val" in result.output
def test_cli_storage_set():
result = _run(storage_group, ["set", "mykey", "myvalue"], None)
assert result.exit_code == 0
assert "Set" in result.output
assert "mykey" in result.output
def test_cli_storage_set_session():
result = _run(storage_group, ["set", "sk", "sv", "--type", "session"], None)
assert result.exit_code == 0
assert "session" in result.output
# ---------------------------------------------------------------------------
# perf commands
# ---------------------------------------------------------------------------
from browser_cli.commands.perf import perf_group
def test_cli_perf_status_no_jobs():
status = {"performanceProfile": "auto", "audible": False,
"throttle": {"batchSize": 10, "pauseMs": 50, "mode": "auto"}, "jobs": []}
result = _run(perf_group, ["status"], status)
assert result.exit_code == 0
assert "auto" in result.output
assert "No running jobs" in result.output
def test_cli_perf_status_with_jobs():
status = {
"performanceProfile": "gentle",
"audible": True,
"throttle": {"batchSize": 5, "pauseMs": 200, "mode": "gentle"},
"jobs": [
{"id": "j1", "command": "session.load", "status": "running",
"current": 3, "total": 10, "percent": 30, "phase": "open_tabs"},
],
}
result = _run(perf_group, ["status"], status)
assert result.exit_code == 0
assert "j1" in result.output
assert "session.load" in result.output
assert "30%" in result.output
def test_cli_perf_profile_set():
result = _run(perf_group, ["profile", "normal"], {"performanceProfile": "normal"})
assert result.exit_code == 0
assert "normal" in result.output
def test_cli_perf_profile_gentle():
result = _run(perf_group, ["profile", "gentle"], {"performanceProfile": "gentle"})
assert result.exit_code == 0
assert "gentle" in result.output
def test_cli_perf_profile_ultra():
result = _run(perf_group, ["profile", "ultra"], {"performanceProfile": "ultra"})
assert result.exit_code == 0
assert "ultra" in result.output
# ---------------------------------------------------------------------------
# navigate commands
# ---------------------------------------------------------------------------
from browser_cli.commands.navigate import nav_group
def test_cli_nav_open():
result = _run(nav_group, ["open", "https://example.com"], {"id": 42, "url": "https://example.com"})
assert result.exit_code == 0
assert "Opened" in result.output
def test_cli_nav_open_bg():
result = _run(nav_group, ["open", "https://example.com", "--bg"], {"id": 42})
assert result.exit_code == 0
assert "Opened" in result.output
def test_cli_nav_open_with_group():
result = _run(nav_group, ["open", "https://example.com", "--group", "work"], {"id": 42})
assert result.exit_code == 0
assert "work" in result.output
def test_cli_nav_open_with_window():
result = _run(nav_group, ["open", "https://example.com", "--window", "main"], {"id": 42})
assert result.exit_code == 0
assert "main" in result.output
def test_cli_nav_reload():
result = _run(nav_group, ["reload"], None)
assert result.exit_code == 0
assert "Reloaded" in result.output
def test_cli_nav_hard_reload():
result = _run(nav_group, ["hard-reload"], None)
assert result.exit_code == 0
assert "Hard reloaded" in result.output
def test_cli_nav_back():
result = _run(nav_group, ["back"], None)
assert result.exit_code == 0
assert "back" in result.output.lower()
def test_cli_nav_forward():
result = _run(nav_group, ["forward"], None)
assert result.exit_code == 0
assert "forward" in result.output.lower()
def test_cli_nav_focus_found():
result = _run(nav_group, ["focus", "example.com"], {"url": "https://example.com"})
assert result.exit_code == 0
assert "Focused" in result.output
def test_cli_nav_focus_not_found():
result = _run(nav_group, ["focus", "no-match"], None)
assert result.exit_code == 0
assert "No tab found" in result.output
def test_cli_nav_open_wait():
result = _run(nav_group, ["open-wait", "https://example.com"],
{"id": 1, "title": "Example Domain", "url": "https://example.com"})
assert result.exit_code == 0
assert "Loaded" in result.output
assert "Example Domain" in result.output
def test_cli_nav_open_wait_no_title():
result = _run(nav_group, ["open-wait", "https://example.com"], {"id": 1})
assert result.exit_code == 0
assert "Loaded" in result.output
def test_cli_nav_wait():
result = _run(nav_group, ["wait"], {"url": "https://example.com", "title": "Example"})
assert result.exit_code == 0
assert "Ready" in result.output
# ---------------------------------------------------------------------------
# navigate commands — with tab_id argument
# ---------------------------------------------------------------------------
def test_cli_nav_reload_with_tab_id():
result = _run(nav_group, ["reload", "42"], None)
assert result.exit_code == 0
def test_cli_nav_back_with_tab_id():
result = _run(nav_group, ["back", "42"], None)
assert result.exit_code == 0
def test_cli_nav_forward_with_tab_id():
result = _run(nav_group, ["forward", "42"], None)
assert result.exit_code == 0
# ---------------------------------------------------------------------------
# tabs commands (CLI module) — single-browser mode
# ---------------------------------------------------------------------------
from browser_cli.commands.tabs import tabs_group
_TABS_MOD = "browser_cli.commands.tabs"
_SAMPLE_TAB = {"id": 1, "windowId": 1, "active": True, "muted": False,
"title": "Example", "url": "https://example.com", "groupId": -1}
def test_cli_tabs_list_empty():
with patch("browser_cli.commands.send_command", return_value=[]), \
patch(f"{_TABS_MOD}._multi_browser_targets", return_value=[]):
result = CliRunner().invoke(tabs_group, ["list"])
assert result.exit_code == 0
assert "No tabs found" in result.output
def test_cli_tabs_list_with_tabs():
with patch("browser_cli.commands.send_command", return_value=[_SAMPLE_TAB]), \
patch(f"{_TABS_MOD}._multi_browser_targets", return_value=[]):
result = CliRunner().invoke(tabs_group, ["list"])
assert result.exit_code == 0
assert "example.com" in result.output
def test_cli_tabs_close_by_id():
with patch("browser_cli.commands.send_command", return_value={"closed": 1}):
result = CliRunner().invoke(tabs_group, ["close", "42"])
assert result.exit_code == 0
assert "Closed 1" in result.output
def test_cli_tabs_close_inactive():
with patch("browser_cli.commands.send_command", return_value={"closed": 3}):
result = CliRunner().invoke(tabs_group, ["close", "--inactive"])
assert result.exit_code == 0
assert "Closed 3" in result.output
def test_cli_tabs_close_duplicates():
with patch("browser_cli.commands.send_command", return_value={"closed": 2}):
result = CliRunner().invoke(tabs_group, ["close", "--duplicates"])
assert result.exit_code == 0
assert "Closed 2" in result.output
def test_cli_tabs_move_forward():
with patch("browser_cli.commands.send_command", return_value=None):
result = CliRunner().invoke(tabs_group, ["move", "42", "--forward"])
assert result.exit_code == 0
assert "Tab moved" in result.output
def test_cli_tabs_move_to_window():
with patch("browser_cli.commands.send_command", return_value=None):
result = CliRunner().invoke(tabs_group, ["move", "42", "--window", "2"])
assert result.exit_code == 0
def test_cli_tabs_active():
with patch("browser_cli.commands.send_command", return_value=None):
result = CliRunner().invoke(tabs_group, ["active", "42"])
assert result.exit_code == 0
assert "42" in result.output
def test_cli_tabs_status():
tab = {"id": 5, "windowId": 1, "active": True, "muted": False,
"title": "MyPage", "url": "https://my.page"}
with patch("browser_cli.commands.send_command", return_value=tab):
result = CliRunner().invoke(tabs_group, ["status"])
assert result.exit_code == 0
assert "MyPage" in result.output
def test_cli_tabs_filter():
with patch("browser_cli.commands.send_command", return_value=[_SAMPLE_TAB]):
result = CliRunner().invoke(tabs_group, ["filter", "example"])
assert result.exit_code == 0
assert "example.com" in result.output
def test_cli_tabs_filter_empty():
with patch("browser_cli.commands.send_command", return_value=[]):
result = CliRunner().invoke(tabs_group, ["filter", "nope"])
assert result.exit_code == 0
assert "No tabs" in result.output
def test_cli_tabs_count():
with patch("browser_cli.commands.send_command", return_value=7), \
patch(f"{_TABS_MOD}._multi_browser_targets", return_value=[]):
result = CliRunner().invoke(tabs_group, ["count"])
assert result.exit_code == 0
assert "7" in result.output
def test_cli_tabs_count_with_pattern():
with patch("browser_cli.commands.send_command", return_value=3), \
patch(f"{_TABS_MOD}._multi_browser_targets", return_value=[]):
result = CliRunner().invoke(tabs_group, ["count", "http"])
assert result.exit_code == 0
assert "3" in result.output
assert "http" in result.output
def test_cli_tabs_query():
with patch("browser_cli.commands.send_command", return_value=[_SAMPLE_TAB]):
result = CliRunner().invoke(tabs_group, ["query", "example"])
assert result.exit_code == 0
assert "example.com" in result.output
def test_cli_tabs_html():
with patch("browser_cli.commands.send_command", return_value="<html><body>hello</body></html>"):
result = CliRunner().invoke(tabs_group, ["html"])
assert result.exit_code == 0
assert "hello" in result.output
def test_cli_tabs_dedupe():
with patch("browser_cli.commands.send_command", return_value={"closed": 4}):
result = CliRunner().invoke(tabs_group, ["dedupe"])
assert result.exit_code == 0
assert "4 duplicate" in result.output
def test_cli_tabs_sort():
with patch("browser_cli.commands.send_command", return_value=None):
result = CliRunner().invoke(tabs_group, ["sort", "--by", "title"])
assert result.exit_code == 0
assert "title" in result.output
def test_cli_tabs_merge_windows():
with patch("browser_cli.commands.send_command", return_value={"moved": 2}):
result = CliRunner().invoke(tabs_group, ["merge-windows"])
assert result.exit_code == 0
assert "2" in result.output
def test_cli_tabs_mute():
with patch("browser_cli.commands.send_command", return_value={"tabId": 5}):
result = CliRunner().invoke(tabs_group, ["mute", "5"])
assert result.exit_code == 0
assert "Muted tab 5" in result.output
def test_cli_tabs_unmute():
with patch("browser_cli.commands.send_command", return_value={"tabId": 5}):
result = CliRunner().invoke(tabs_group, ["unmute", "5"])
assert result.exit_code == 0
assert "Unmuted tab 5" in result.output
def test_cli_tabs_pin():
with patch("browser_cli.commands.send_command", return_value={"tabId": 5}):
result = CliRunner().invoke(tabs_group, ["pin", "5"])
assert result.exit_code == 0
assert "Pinned tab 5" in result.output
def test_cli_tabs_unpin():
with patch("browser_cli.commands.send_command", return_value={"tabId": 5}):
result = CliRunner().invoke(tabs_group, ["unpin", "5"])
assert result.exit_code == 0
assert "Unpinned tab 5" in result.output
def test_cli_tabs_watch_url():
with patch("browser_cli.commands.send_command", return_value={"url": "https://done.com"}):
result = CliRunner().invoke(tabs_group, ["watch-url", "done\\.com"])
assert result.exit_code == 0
assert "done.com" in result.output
def test_cli_tabs_screenshot_stdout():
with patch("browser_cli.commands.send_command", return_value={"dataUrl": "data:image/png;base64,abc"}):
result = CliRunner().invoke(tabs_group, ["screenshot"])
assert result.exit_code == 0
assert "data:image/png" in result.output
def test_cli_tabs_screenshot_to_file(tmp_path):
import base64
png_bytes = b"\x89PNG\r\n\x1a\n" + b"\x00" * 8 # minimal header
data_url = "data:image/png;base64," + base64.b64encode(png_bytes).decode()
out = tmp_path / "shot.png"
with patch("browser_cli.commands.send_command", return_value={"dataUrl": data_url}):
result = CliRunner().invoke(tabs_group, ["screenshot", str(out)])
assert result.exit_code == 0
assert "saved" in result.output.lower()
assert out.exists()
# ---------------------------------------------------------------------------
# groups commands (CLI module)
# ---------------------------------------------------------------------------
from browser_cli.commands.groups import group_group
_GROUPS_MOD = "browser_cli.commands.groups"
_SAMPLE_GROUP = {"id": 10, "title": "Work", "color": "blue", "collapsed": False, "tabCount": 3}
def test_cli_groups_list_empty():
with patch("browser_cli.commands.send_command", return_value=[]), \
patch(f"{_GROUPS_MOD}._multi_browser_targets", return_value=[]):
result = CliRunner().invoke(group_group, ["list"])
assert result.exit_code == 0
assert "No groups found" in result.output
def test_cli_groups_list_with_groups():
with patch("browser_cli.commands.send_command", return_value=[_SAMPLE_GROUP]), \
patch(f"{_GROUPS_MOD}._multi_browser_targets", return_value=[]):
result = CliRunner().invoke(group_group, ["list"])
assert result.exit_code == 0
assert "Work" in result.output
def test_cli_groups_tabs():
tabs = [_SAMPLE_TAB]
with patch("browser_cli.commands.send_command", return_value=tabs):
result = CliRunner().invoke(group_group, ["tabs", "10"])
assert result.exit_code == 0
assert "example.com" in result.output
def test_cli_groups_tabs_empty():
with patch("browser_cli.commands.send_command", return_value=[]):
result = CliRunner().invoke(group_group, ["tabs", "10"])
assert result.exit_code == 0
assert "No tabs" in result.output
def test_cli_groups_count():
with patch("browser_cli.commands.send_command", return_value=5), \
patch(f"{_GROUPS_MOD}._multi_browser_targets", return_value=[]):
result = CliRunner().invoke(group_group, ["count"])
assert result.exit_code == 0
assert "5" in result.output
def test_cli_groups_query_empty():
with patch("browser_cli.commands.send_command", return_value=[]):
result = CliRunner().invoke(group_group, ["query", "nothing"])
assert result.exit_code == 0
assert "No groups" in result.output
def test_cli_groups_query_found():
with patch("browser_cli.commands.send_command", return_value=[_SAMPLE_GROUP]):
result = CliRunner().invoke(group_group, ["query", "Work"])
assert result.exit_code == 0
assert "Work" in result.output
def test_cli_groups_close():
with patch("browser_cli.commands.send_command", return_value=None):
result = CliRunner().invoke(group_group, ["close", "10"])
assert result.exit_code == 0
assert "10" in result.output
def test_cli_groups_create():
with patch("browser_cli.commands.send_command", return_value={"id": 42}):
result = CliRunner().invoke(group_group, ["create", "Research"])
assert result.exit_code == 0
assert "Research" in result.output
assert "42" in result.output
def test_cli_groups_add_tab_no_url():
with patch("browser_cli.commands.send_command", return_value={"tabId": 7}):
result = CliRunner().invoke(group_group, ["add-tab", "Work"])
assert result.exit_code == 0
assert "Work" in result.output
def test_cli_groups_add_tab_with_url():
with patch("browser_cli.commands.send_command", return_value={"tabId": 9}):
result = CliRunner().invoke(group_group, ["add-tab", "Work", "https://docs.example.com"])
assert result.exit_code == 0
assert "docs.example.com" in result.output
def test_cli_groups_move_forward():
with patch("browser_cli.commands.send_command", return_value={"moved": True}):
result = CliRunner().invoke(group_group, ["move", "10", "--forward"])
assert result.exit_code == 0
assert "forward" in result.output
def test_cli_groups_move_backward():
with patch("browser_cli.commands.send_command", return_value={"moved": True}):
result = CliRunner().invoke(group_group, ["move", "10", "--backward"])
assert result.exit_code == 0
assert "backward" in result.output
def test_cli_groups_move_no_direction():
with patch("browser_cli.commands.send_command", return_value=None):
result = CliRunner().invoke(group_group, ["move", "10"])
assert result.exit_code != 0
def test_cli_groups_move_already_at_end():
with patch("browser_cli.commands.send_command", return_value={"moved": False}):
result = CliRunner().invoke(group_group, ["move", "10", "--forward"])
assert result.exit_code == 0
assert "already at" in result.output
# ---------------------------------------------------------------------------
# session commands (CLI module)
# ---------------------------------------------------------------------------
from browser_cli.commands.session import session_group
_SESSION_MOD = "browser_cli.commands.session"
def test_cli_session_save():
with patch("browser_cli.commands.send_command", return_value={"tabs": 5}):
result = CliRunner().invoke(session_group, ["save", "work"])
assert result.exit_code == 0
assert "work" in result.output
assert "5" in result.output
def test_cli_session_load():
with patch("browser_cli.commands.send_command", return_value={"tabs": 8}):
result = CliRunner().invoke(session_group, ["load", "work"])
assert result.exit_code == 0
assert "work" in result.output
assert "8" in result.output
def test_cli_session_load_background():
with patch("browser_cli.commands.send_command", return_value={"jobId": "j-abc", "status": "running"}):
result = CliRunner().invoke(session_group, ["load", "work", "--background"])
assert result.exit_code == 0
assert "j-abc" in result.output
def test_cli_session_load_lazy():
with patch("browser_cli.commands.send_command", return_value={"tabs": 20}):
result = CliRunner().invoke(session_group, ["load", "work", "--lazy", "--eager-tabs", "5"])
assert result.exit_code == 0
def test_cli_session_diff_has_changes():
diff = {"added": ["https://new.com"], "removed": ["https://old.com"]}
with patch("browser_cli.commands.send_command", return_value=diff):
result = CliRunner().invoke(session_group, ["diff", "a", "b"])
assert result.exit_code == 0
assert "new.com" in result.output
assert "old.com" in result.output
def test_cli_session_diff_identical():
with patch("browser_cli.commands.send_command", return_value={"added": [], "removed": []}):
result = CliRunner().invoke(session_group, ["diff", "a", "b"])
assert result.exit_code == 0
assert "identical" in result.output
def test_cli_session_diff_no_data():
with patch("browser_cli.commands.send_command", return_value=None):
result = CliRunner().invoke(session_group, ["diff", "a", "b"])
assert result.exit_code == 0
assert "No diff" in result.output
def test_cli_session_list_empty():
with patch("browser_cli.commands.send_command", return_value=[]), \
patch(f"{_SESSION_MOD}._multi_browser_targets", return_value=[]):
result = CliRunner().invoke(session_group, ["list"])
assert result.exit_code == 0
assert "No saved sessions" in result.output
def test_cli_session_list_with_sessions():
sessions = [{"name": "work", "tabs": 3, "savedAt": 1700000000000}]
with patch("browser_cli.commands.send_command", return_value=sessions), \
patch(f"{_SESSION_MOD}._multi_browser_targets", return_value=[]):
result = CliRunner().invoke(session_group, ["list"])
assert result.exit_code == 0
assert "work" in result.output
def test_cli_session_remove():
with patch("browser_cli.commands.send_command", return_value=None):
result = CliRunner().invoke(session_group, ["remove", "old-session"])
assert result.exit_code == 0
assert "old-session" in result.output
def test_cli_session_job_status_running():
with patch("browser_cli.commands.send_command", return_value={"status": "running", "percent": 42}):
result = CliRunner().invoke(session_group, ["job-status", "j-xyz"])
assert result.exit_code == 0
assert "running" in result.output
def test_cli_session_job_status_with_error():
with patch("browser_cli.commands.send_command", return_value={"status": "failed", "error": "something broke"}):
result = CliRunner().invoke(session_group, ["job-status", "j-bad"])
assert result.exit_code == 0
assert "something broke" in result.output
def test_cli_session_job_status_with_result():
with patch("browser_cli.commands.send_command", return_value={"status": "done", "result": "Opened 5 tabs"}):
result = CliRunner().invoke(session_group, ["job-status", "j-done"])
assert result.exit_code == 0
assert "Opened 5 tabs" in result.output
def test_cli_session_job_cancel():
with patch("browser_cli.commands.send_command", return_value=None):
result = CliRunner().invoke(session_group, ["job-cancel", "j-running"])
assert result.exit_code == 0
assert "j-running" in result.output
def test_cli_session_auto_save_on():
with patch("browser_cli.commands.send_command", return_value=None):
result = CliRunner().invoke(session_group, ["auto-save", "on"])
assert result.exit_code == 0
assert "on" in result.output
def test_cli_session_auto_save_off():
with patch("browser_cli.commands.send_command", return_value=None):
result = CliRunner().invoke(session_group, ["auto-save", "off"])
assert result.exit_code == 0
assert "off" in result.output
# ---------------------------------------------------------------------------
# multi-browser error paths (None targets → "Cannot resolve" exit)
# ---------------------------------------------------------------------------
from browser_cli.client import BrowserTarget
def _fake_target(name="browser-a"):
return BrowserTarget(profile=name, display_name=name, socket_path="/tmp/fake.sock")
def test_cli_tabs_list_multi_browser_all_none():
"""If every multi-browser target returns None, show error and exit 1."""
target = _fake_target()
with patch("browser_cli.commands.send_command", return_value=None), \
patch(f"{_TABS_MOD}._multi_browser_targets", return_value=[target]), \
patch(f"{_TABS_MOD}._handle_multi", return_value=None):
result = CliRunner().invoke(tabs_group, ["list"])
assert result.exit_code != 0
def test_cli_tabs_count_multi_browser_all_none():
"""If every multi-browser count returns None, show error and exit 1."""
target = _fake_target()
with patch("browser_cli.commands.send_command", return_value=None), \
patch(f"{_TABS_MOD}._multi_browser_targets", return_value=[target]), \
patch(f"{_TABS_MOD}._handle_multi", return_value=None):
result = CliRunner().invoke(tabs_group, ["count"])
assert result.exit_code != 0
def test_cli_groups_list_multi_browser_all_none():
"""If every multi-browser group list returns None, exit 1."""
target = _fake_target()
with patch("browser_cli.commands.send_command", return_value=None), \
patch(f"{_GROUPS_MOD}._multi_browser_targets", return_value=[target]), \
patch(f"{_GROUPS_MOD}._handle_multi", return_value=None):
result = CliRunner().invoke(group_group, ["list"])
assert result.exit_code != 0
def test_cli_groups_count_multi_browser_all_none():
"""If every multi-browser group count returns None, exit 1."""
target = _fake_target()
with patch("browser_cli.commands.send_command", return_value=None), \
patch(f"{_GROUPS_MOD}._multi_browser_targets", return_value=[target]), \
patch(f"{_GROUPS_MOD}._handle_multi", return_value=None):
result = CliRunner().invoke(group_group, ["count"])
assert result.exit_code != 0
def test_cli_session_list_multi_browser_all_none():
"""If every multi-browser session list returns None, exit 1."""
target = _fake_target()
with patch("browser_cli.commands.send_command", return_value=None), \
patch(f"{_SESSION_MOD}._multi_browser_targets", return_value=[target]), \
patch(f"{_SESSION_MOD}._handle_multi", return_value=None):
result = CliRunner().invoke(session_group, ["list"])
assert result.exit_code != 0
# ---------------------------------------------------------------------------
# tabs screenshot error paths
# ---------------------------------------------------------------------------
def test_cli_tabs_screenshot_bad_dataurl(tmp_path):
"""Screenshot command exits non-zero when dataUrl has wrong format."""
out = tmp_path / "bad.png"
with patch("browser_cli.commands.send_command", return_value={"dataUrl": "not-a-dataurl"}):
result = CliRunner().invoke(tabs_group, ["screenshot", str(out)])
assert result.exit_code != 0
def test_cli_tabs_screenshot_bad_base64(tmp_path):
"""Screenshot command exits non-zero when base64 data is corrupt."""
out = tmp_path / "bad.png"
with patch("browser_cli.commands.send_command", return_value={"dataUrl": "data:image/png;base64,!!!NOT_VALID_BASE64!!!"}):
result = CliRunner().invoke(tabs_group, ["screenshot", str(out)])
assert result.exit_code != 0
# ---------------------------------------------------------------------------
# windows commands (CLI module)
# ---------------------------------------------------------------------------
from browser_cli.commands.windows import windows_group
_WINDOWS_MOD = "browser_cli.commands.windows"
_SAMPLE_WINDOW = {"id": 1, "alias": "main", "tabCount": 5, "state": "normal"}
def test_cli_windows_list_empty():
with patch("browser_cli.commands.send_command", return_value=[]), \
patch(f"{_WINDOWS_MOD}._multi_browser_targets", return_value=[]):
result = CliRunner().invoke(windows_group, ["list"])
assert result.exit_code == 0
assert "No windows found" in result.output
def test_cli_windows_list_with_windows():
with patch("browser_cli.commands.send_command", return_value=[_SAMPLE_WINDOW]), \
patch(f"{_WINDOWS_MOD}._multi_browser_targets", return_value=[]):
result = CliRunner().invoke(windows_group, ["list"])
assert result.exit_code == 0
assert "main" in result.output
def test_cli_windows_list_multi_all_none():
target = _fake_target()
with patch("browser_cli.commands.send_command", return_value=None), \
patch(f"{_WINDOWS_MOD}._multi_browser_targets", return_value=[target]), \
patch(f"{_WINDOWS_MOD}._handle_multi", return_value=None):
result = CliRunner().invoke(windows_group, ["list"])
assert result.exit_code != 0
def test_cli_windows_rename():
with patch("browser_cli.commands.send_command", return_value=None):
result = CliRunner().invoke(windows_group, ["rename", "1", "work"])
assert result.exit_code == 0
assert "work" in result.output
assert "1" in result.output
def test_cli_windows_close():
with patch("browser_cli.commands.send_command", return_value=None):
result = CliRunner().invoke(windows_group, ["close", "2"])
assert result.exit_code == 0
assert "2" in result.output
def test_cli_windows_open_no_url():
with patch("browser_cli.commands.send_command", return_value={"id": 5}):
result = CliRunner().invoke(windows_group, ["open"])
assert result.exit_code == 0
assert "5" in result.output
def test_cli_windows_open_with_url():
with patch("browser_cli.commands.send_command", return_value={"id": 6}):
result = CliRunner().invoke(windows_group, ["open", "https://example.com"])
assert result.exit_code == 0
assert "example.com" in result.output
# ---------------------------------------------------------------------------
# commands/__init__.py error paths
# ---------------------------------------------------------------------------
from browser_cli.commands import _handle, _handle_multi
from browser_cli.client import BrowserNotConnected
def test_handle_raises_system_exit_on_browser_not_connected():
"""_handle converts BrowserNotConnected into SystemExit(1)."""
with patch("browser_cli.commands.send_command", side_effect=BrowserNotConnected("no socket")):
with pytest.raises(SystemExit):
_handle("tabs.list")
def test_handle_raises_system_exit_on_runtime_error():
"""_handle converts RuntimeError into SystemExit(1)."""
with patch("browser_cli.commands.send_command", side_effect=RuntimeError("browser blew up")):
with pytest.raises(SystemExit):
_handle("tabs.list")
def test_handle_multi_returns_none_on_error():
"""_handle_multi silently returns None on BrowserNotConnected."""
with patch("browser_cli.commands.send_command", side_effect=BrowserNotConnected("gone")):
result = _handle_multi("tabs.list")
assert result is None
def test_handle_multi_returns_none_on_runtime_error():
"""_handle_multi silently returns None on RuntimeError."""
with patch("browser_cli.commands.send_command", side_effect=RuntimeError("oops")):
result = _handle_multi("tabs.list")
assert result is None
def test_handle_multi_with_remote():
"""_handle_multi routes through remote when remote arg is set."""
with patch("browser_cli.commands.send_command", return_value={"ok": True}) as mock_send:
result = _handle_multi("tabs.list", profile="brave", remote="host:8765")
assert result == {"ok": True}
mock_send.assert_called_once_with("tabs.list", {}, profile="brave", remote="host:8765")
+103
View File
@@ -0,0 +1,103 @@
"""Integration tests for cookies.* commands — require a live browser."""
import time
def test_cookies_list_returns_list(browser, http_tab):
"""cookies.list returns a list (may be empty on a plain https://example.com)."""
browser("tabs.active", {"tabId": http_tab["id"]})
cookies = browser("cookies.list", {})
assert isinstance(cookies, list)
def test_cookies_list_has_required_fields(browser, http_tab):
"""Every cookie returned has at least name, domain and path fields."""
browser("tabs.active", {"tabId": http_tab["id"]})
# Set a known cookie so the list is non-empty
browser("cookies.set", {
"url": "https://example.com",
"name": "__pytest_field_check",
"value": "1",
})
cookies = browser("cookies.list", {"url": "https://example.com"})
assert isinstance(cookies, list)
assert len(cookies) > 0
for c in cookies:
assert "name" in c
assert "domain" in c
assert "path" in c
def test_cookies_set_and_list(browser, http_tab):
"""Set a cookie and verify it appears in the list."""
browser("tabs.active", {"tabId": http_tab["id"]})
cookie_name = "__pytest_set_test"
cookie_value = "hello-pytest"
browser("cookies.set", {
"url": "https://example.com",
"name": cookie_name,
"value": cookie_value,
})
cookies = browser("cookies.list", {"url": "https://example.com"})
found = next((c for c in cookies if c.get("name") == cookie_name), None)
assert found is not None, f"Cookie '{cookie_name}' not found after set"
assert found["value"] == cookie_value
def test_cookies_get(browser, http_tab):
"""Get a single cookie by URL + name."""
browser("tabs.active", {"tabId": http_tab["id"]})
name = "__pytest_get_test"
value = "get-value-42"
browser("cookies.set", {"url": "https://example.com", "name": name, "value": value})
cookie = browser("cookies.get", {"url": "https://example.com", "name": name})
assert cookie is not None
assert cookie.get("value") == value
def test_cookies_get_missing_returns_none(browser, http_tab):
"""Getting a non-existent cookie returns None."""
browser("tabs.active", {"tabId": http_tab["id"]})
cookie = browser("cookies.get", {
"url": "https://example.com",
"name": "__pytest_no_such_cookie_zzz",
})
assert cookie is None
def test_cookies_list_filter_by_domain(browser, http_tab):
"""Filtering by domain only returns matching cookies."""
browser("tabs.active", {"tabId": http_tab["id"]})
browser("cookies.set", {
"url": "https://example.com",
"name": "__pytest_domain_filter",
"value": "yes",
})
cookies = browser("cookies.list", {"domain": "example.com"})
assert isinstance(cookies, list)
for c in cookies:
assert "example.com" in c.get("domain", "")
def test_cookies_list_filter_by_name(browser, http_tab):
"""Filtering by name only returns cookies with that name."""
browser("tabs.active", {"tabId": http_tab["id"]})
name = "__pytest_name_filter_unique"
browser("cookies.set", {"url": "https://example.com", "name": name, "value": "y"})
cookies = browser("cookies.list", {"name": name})
assert isinstance(cookies, list)
assert len(cookies) > 0
for c in cookies:
assert c["name"] == name
def test_cookies_set_with_secure_flag(browser, http_tab):
"""Setting a cookie with secure=True persists the secure attribute."""
browser("tabs.active", {"tabId": http_tab["id"]})
name = "__pytest_secure_cookie"
browser("cookies.set", {
"url": "https://example.com",
"name": name,
"value": "secured",
"secure": True,
})
cookies = browser("cookies.list", {"url": "https://example.com", "name": name})
found = next((c for c in cookies if c["name"] == name), None)
assert found is not None
assert found.get("secure") is True
+207
View File
@@ -58,3 +58,210 @@ def test_dom_attr_html_lang(browser, http_tab):
assert isinstance(langs, list)
# html element exists so we get exactly one entry (may be empty string if no lang attr)
assert len(langs) <= 1
# ---------------------------------------------------------------------------
# dom.eval
# ---------------------------------------------------------------------------
def test_dom_eval_returns_string(browser, http_tab):
"""Evaluating document.title returns the page title as a string."""
browser("tabs.active", {"tabId": http_tab["id"]})
result = browser("dom.eval", {"code": "document.title", "tabId": http_tab["id"]})
assert isinstance(result, str)
def test_dom_eval_arithmetic(browser, http_tab):
"""Evaluating a JS expression returns the computed value."""
browser("tabs.active", {"tabId": http_tab["id"]})
result = browser("dom.eval", {"code": "2 + 2", "tabId": http_tab["id"]})
assert result == 4
def test_dom_eval_returns_null_for_void(browser, http_tab):
"""Evaluating a void expression returns None."""
browser("tabs.active", {"tabId": http_tab["id"]})
result = browser("dom.eval", {"code": "void 0", "tabId": http_tab["id"]})
assert result is None
def test_dom_eval_returns_dict(browser, http_tab):
"""Evaluating an object expression returns a dict."""
browser("tabs.active", {"tabId": http_tab["id"]})
result = browser("dom.eval", {"code": "({a: 1, b: 2})", "tabId": http_tab["id"]})
assert isinstance(result, dict)
assert result.get("a") == 1
assert result.get("b") == 2
def test_dom_eval_dom_read(browser, http_tab):
"""Can read a property of a DOM element via eval."""
browser("tabs.active", {"tabId": http_tab["id"]})
result = browser("dom.eval", {"code": "document.querySelector('h1') ? document.querySelector('h1').textContent : null", "tabId": http_tab["id"]})
# result is either a string (h1 text) or None — both are valid
assert result is None or isinstance(result, str)
# ---------------------------------------------------------------------------
# dom.scroll
# ---------------------------------------------------------------------------
def test_dom_scroll_to_coordinates(browser, http_tab):
"""Scrolling to (x, y) coordinates does not raise."""
browser("tabs.active", {"tabId": http_tab["id"]})
result = browser("dom.scroll", {"x": 0, "y": 0})
assert result is None or isinstance(result, (dict, bool))
def test_dom_scroll_to_selector(browser, http_tab):
"""Scrolling to an existing selector does not raise."""
browser("tabs.active", {"tabId": http_tab["id"]})
result = browser("dom.scroll", {"selector": "body"})
assert result is None or isinstance(result, (dict, bool))
# ---------------------------------------------------------------------------
# dom.wait_for
# ---------------------------------------------------------------------------
def test_dom_wait_for_existing_element(browser, http_tab):
"""wait_for an element that already exists returns quickly."""
browser("tabs.active", {"tabId": http_tab["id"]})
result = browser("dom.wait_for", {
"selector": "html",
"timeout": 5000,
"visible": False,
"hidden": False,
"tabId": http_tab["id"],
})
# Returns None or a dict on success
assert result is None or isinstance(result, dict)
def test_dom_wait_for_visible(browser, http_tab):
"""wait_for visible=True on a visible element succeeds."""
browser("tabs.active", {"tabId": http_tab["id"]})
result = browser("dom.wait_for", {
"selector": "body",
"timeout": 5000,
"visible": True,
"hidden": False,
"tabId": http_tab["id"],
})
assert result is None or isinstance(result, dict)
# ---------------------------------------------------------------------------
# dom.focus
# ---------------------------------------------------------------------------
def test_dom_focus_element(browser, http_tab):
"""Focusing an existing element does not raise."""
browser("tabs.active", {"tabId": http_tab["id"]})
result = browser("dom.focus", {"selector": "body"})
assert result is None or isinstance(result, (dict, bool))
# ---------------------------------------------------------------------------
# dom.hover
# ---------------------------------------------------------------------------
def test_dom_hover_element(browser, http_tab):
"""Hovering over an existing element does not raise."""
browser("tabs.active", {"tabId": http_tab["id"]})
result = browser("dom.hover", {"selector": "body"})
assert result is None or isinstance(result, (dict, bool))
# ---------------------------------------------------------------------------
# dom.type / dom.clear (use a data: URL tab with an <input>)
# ---------------------------------------------------------------------------
def test_dom_type_into_input(browser, http_tab):
"""Type text into an injected input field and read it back via eval."""
browser("tabs.active", {"tabId": http_tab["id"]})
# Inject a fresh input element with a unique id
input_id = "__pytest_type_input"
browser("dom.eval", {
"code": f"(function(){{ var e=document.getElementById('{input_id}'); if(!e){{e=document.createElement('input');e.id='{input_id}';e.type='text';document.body.appendChild(e);}} return true; }})()",
"tabId": http_tab["id"],
})
browser("dom.type", {"selector": f"#{input_id}", "text": "hello"})
value = browser("dom.eval", {"code": f"document.getElementById('{input_id}').value", "tabId": http_tab["id"]})
assert value == "hello"
def test_dom_clear_input(browser, http_tab):
"""Clear an input field sets its value to empty string."""
browser("tabs.active", {"tabId": http_tab["id"]})
input_id = "__pytest_clear_input"
browser("dom.eval", {
"code": f"(function(){{ var e=document.getElementById('{input_id}'); if(!e){{e=document.createElement('input');e.id='{input_id}';e.type='text';document.body.appendChild(e);}} e.value='prefilled'; return true; }})()",
"tabId": http_tab["id"],
})
browser("dom.clear", {"selector": f"#{input_id}"})
value = browser("dom.eval", {"code": f"document.getElementById('{input_id}').value", "tabId": http_tab["id"]})
assert value == ""
# ---------------------------------------------------------------------------
# dom.key
# ---------------------------------------------------------------------------
def test_dom_key_event_does_not_raise(browser, http_tab):
"""Sending a key event to the body does not raise."""
browser("tabs.active", {"tabId": http_tab["id"]})
result = browser("dom.key", {"key": "Tab"})
assert result is None or isinstance(result, (dict, bool))
def test_dom_key_with_selector(browser, http_tab):
"""Sending a key event to a specific selector does not raise."""
browser("tabs.active", {"tabId": http_tab["id"]})
result = browser("dom.key", {"key": "Escape", "selector": "body"})
assert result is None or isinstance(result, (dict, bool))
# ---------------------------------------------------------------------------
# dom.select (requires a <select> element)
# ---------------------------------------------------------------------------
def test_dom_select_dropdown(browser, http_tab):
"""Setting a <select> value changes it and it can be read back."""
browser("tabs.active", {"tabId": http_tab["id"]})
sel_id = "__pytest_select"
browser("dom.eval", {
"code": (
f"(function(){{"
f" var s=document.getElementById('{sel_id}');"
f" if(!s){{"
f" s=document.createElement('select');s.id='{sel_id}';"
f" ['a','b','c'].forEach(function(v){{var o=document.createElement('option');o.value=v;o.text=v;s.appendChild(o);}});"
f" document.body.appendChild(s);"
f" }} return true;"
f"}})()"
),
"tabId": http_tab["id"],
})
browser("dom.select", {"selector": f"#{sel_id}", "value": "b"})
value = browser("dom.eval", {"code": f"document.getElementById('{sel_id}').value", "tabId": http_tab["id"]})
assert value == "b"
# ---------------------------------------------------------------------------
# dom.check / dom.uncheck
# ---------------------------------------------------------------------------
def test_dom_check_and_uncheck(browser, http_tab):
"""Checking and unchecking a checkbox toggles its checked state."""
browser("tabs.active", {"tabId": http_tab["id"]})
cb_id = "__pytest_checkbox"
browser("dom.eval", {
"code": (
f"(function(){{"
f" var c=document.getElementById('{cb_id}');"
f" if(!c){{"
f" c=document.createElement('input');c.id='{cb_id}';c.type='checkbox';"
f" document.body.appendChild(c);"
f" }} c.checked=false; return true;"
f"}})()"
),
"tabId": http_tab["id"],
})
browser("dom.check", {"selector": f"#{cb_id}"})
checked = browser("dom.eval", {"code": f"document.getElementById('{cb_id}').checked", "tabId": http_tab["id"]})
assert checked is True
browser("dom.uncheck", {"selector": f"#{cb_id}"})
checked = browser("dom.eval", {"code": f"document.getElementById('{cb_id}').checked", "tabId": http_tab["id"]})
assert checked is False
+221 -9
View File
@@ -6,11 +6,9 @@ import pytest
import browser_cli.native_host as native_host
def _raise_system_exit(code: int):
raise SystemExit(code)
def test_cleanup_removes_socket_and_registry_entry(monkeypatch, tmp_path):
alias = "work"
socket_path = tmp_path / "work.sock"
@@ -27,7 +25,6 @@ def test_cleanup_removes_socket_and_registry_entry(monkeypatch, tmp_path):
assert not socket_path.exists()
assert json.loads(registry_path.read_text()) == {"other": str(tmp_path / "other.sock")}
def test_stdin_reader_cleans_up_on_eof(monkeypatch):
cleaned = []
@@ -41,7 +38,6 @@ def test_stdin_reader_cleans_up_on_eof(monkeypatch):
assert cleaned == ["work"]
def test_cleanup_windows_skips_socket_unlink(monkeypatch, tmp_path):
registry_path = tmp_path / "registry.json"
registry_path.write_text(json.dumps({"work": r"\\.\pipe\browser-cli-work"}))
@@ -53,7 +49,6 @@ def test_cleanup_windows_skips_socket_unlink(monkeypatch, tmp_path):
assert json.loads(registry_path.read_text()) == {}
def test_stdin_reader_cleans_up_on_bye(monkeypatch):
cleaned = []
messages = iter([{"type": "bye"}])
@@ -68,7 +63,6 @@ def test_stdin_reader_cleans_up_on_bye(monkeypatch):
assert cleaned == ["work"]
def test_stdin_reader_routes_response_messages(monkeypatch):
response_queue = native_host.queue.Queue()
native_host.PENDING["msg-1"] = response_queue
@@ -85,7 +79,6 @@ def test_stdin_reader_routes_response_messages(monkeypatch):
assert response_queue.get_nowait() == {"id": "msg-1", "success": True}
native_host.PENDING.clear()
def test_collect_paged_browser_command_accumulates_pages(monkeypatch):
calls = []
pages = iter([
@@ -110,7 +103,6 @@ def test_collect_paged_browser_command_accumulates_pages(monkeypatch):
assert all(call["args"]["foo"] == "bar" for call in calls)
assert all(call["id"] != "orig" for call in calls)
def test_collect_paged_browser_command_passes_through_non_paged_response(monkeypatch):
monkeypatch.setattr(native_host, "_send_browser_command", lambda cmd: {"id": cmd["id"], "success": True, "data": {"value": 1}})
@@ -118,7 +110,6 @@ def test_collect_paged_browser_command_passes_through_non_paged_response(monkeyp
assert result == {"id": "orig", "success": True, "data": {"value": 1}}
def test_handle_browser_command_pages_known_list_commands(monkeypatch):
seen = []
@@ -128,3 +119,224 @@ def test_handle_browser_command_pages_known_list_commands(monkeypatch):
assert result == {"success": True, "data": []}
assert seen[0]["command"] == "tabs.list"
def test_handle_browser_command_sends_non_pageable_directly(monkeypatch):
seen = []
monkeypatch.setattr(native_host, "_send_browser_command", lambda cmd: seen.append(cmd) or {"success": True, "data": "ok"})
result = native_host._handle_browser_command({"id": "x", "command": "navigate.open", "args": {}})
assert result == {"success": True, "data": "ok"}
assert seen[0]["command"] == "navigate.open"
# ---------------------------------------------------------------------------
# _read_exact_stream
# ---------------------------------------------------------------------------
def test_read_exact_stream_full_read():
"""Returns the exact bytes when stream delivers them in one shot."""
import io
stream = io.BytesIO(b"hello")
assert native_host._read_exact_stream(stream, 5) == b"hello"
def test_read_exact_stream_partial_chunks():
"""Accumulates multiple short chunks until n bytes are read."""
import io
class _ChunkyStream:
def __init__(self, data, chunk_size):
self._data = data
self._pos = 0
self._chunk_size = chunk_size
def read(self, n):
end = min(self._pos + self._chunk_size, len(self._data))
chunk = self._data[self._pos:end]
self._pos = end
return chunk
stream = _ChunkyStream(b"abcdefgh", 3)
assert native_host._read_exact_stream(stream, 8) == b"abcdefgh"
def test_read_exact_stream_eof_returns_none():
"""Returns None if stream is exhausted before n bytes are delivered."""
import io
stream = io.BytesIO(b"ab") # only 2 bytes, asking for 4
assert native_host._read_exact_stream(stream, 4) is None
def test_read_exact_stream_immediate_eof():
"""Returns None on an empty stream."""
import io
stream = io.BytesIO(b"")
assert native_host._read_exact_stream(stream, 1) is None
# ---------------------------------------------------------------------------
# write_native_message / read_native_message round-trip
# ---------------------------------------------------------------------------
def test_write_and_read_native_message_roundtrip():
"""write_native_message followed by read_native_message recovers the original dict."""
import io
buf = io.BytesIO()
msg = {"id": "abc", "command": "tabs.list", "args": {}}
native_host.write_native_message(buf, msg)
buf.seek(0)
result = native_host.read_native_message(buf)
assert result == msg
def test_read_native_message_eof_at_length_prefix():
"""Returns None when the stream is empty (no length prefix)."""
import io
stream = io.BytesIO(b"")
assert native_host.read_native_message(stream) is None
def test_read_native_message_eof_at_body():
"""Returns None when the body is truncated after reading the length prefix."""
import io
import struct
# Write a 10-byte length prefix but only 5 bytes of body
buf = struct.pack("<I", 10) + b"hello"
stream = io.BytesIO(buf)
assert native_host.read_native_message(stream) is None
# ---------------------------------------------------------------------------
# _recv_exact / _recv_all / _send_all
# ---------------------------------------------------------------------------
def test_recv_exact_accumulates_data():
"""_recv_exact receives exactly n bytes from a socket-like object."""
class _FakeSock:
def __init__(self, data):
self._data = data
self._pos = 0
def recv(self, n):
chunk = self._data[self._pos:self._pos + n]
self._pos += len(chunk)
return chunk
sock = _FakeSock(b"0123456789")
assert native_host._recv_exact(sock, 5) == b"01234"
assert native_host._recv_exact(sock, 5) == b"56789"
def test_recv_exact_eof_returns_none():
class _EmptySock:
def recv(self, n):
return b""
assert native_host._recv_exact(_EmptySock(), 4) is None
def test_send_all_and_recv_all():
"""_send_all frames data with length prefix; _recv_all strips it."""
import socket
a, b = socket.socketpair()
try:
payload = b'{"command": "tabs.list"}'
native_host._send_all(a, payload)
received = native_host._recv_all(b)
assert received == payload
finally:
a.close()
b.close()
def test_recv_all_truncated_body():
"""_recv_all returns None when the body is shorter than the prefix promises."""
import socket
import struct
a, b = socket.socketpair()
try:
# Send a length of 100 but only 4 bytes of body
a.sendall(struct.pack("<I", 100) + b"tiny")
a.close()
result = native_host._recv_all(b)
assert result is None
finally:
b.close()
# ---------------------------------------------------------------------------
# _send_browser_command — timeout path
# ---------------------------------------------------------------------------
def test_send_browser_command_timeout(monkeypatch):
"""_send_browser_command returns an error dict when the response queue times out."""
import io
buf = io.BytesIO()
monkeypatch.setattr(native_host.sys, "stdout", SimpleNamespace(buffer=buf))
# Do not put anything into the response queue → timeout after 0 s
result = native_host._send_browser_command({"id": "t1", "command": "test", "args": {}}, timeout=0)
assert result["success"] is False
assert "timeout" in result["error"]
# Clean up PENDING
native_host.PENDING.clear()
# ---------------------------------------------------------------------------
# _collect_paged_browser_command — error and loop-guard paths
# ---------------------------------------------------------------------------
def test_collect_paged_browser_command_propagates_error(monkeypatch):
"""If _send_browser_command returns success=False the error is propagated."""
monkeypatch.setattr(
native_host, "_send_browser_command",
lambda cmd: {"id": cmd["id"], "success": False, "error": "extension crash"},
)
result = native_host._collect_paged_browser_command({"id": "e1", "command": "tabs.list", "args": {}})
assert result["success"] is False
assert "extension crash" in result["error"]
def test_collect_paged_browser_command_max_pages_guard(monkeypatch):
"""If paging never ends, the loop guard kicks in and returns an error."""
monkeypatch.setattr(native_host, "PAGE_SIZE", 1)
call_count = [0]
def _infinite_pages(cmd):
call_count[0] += 1
return {
"id": cmd["id"],
"success": True,
"data": {"__browserCliPage": True, "items": [call_count[0]], "total": 9999, "nextOffset": call_count[0]},
}
monkeypatch.setattr(native_host, "_send_browser_command", _infinite_pages)
result = native_host._collect_paged_browser_command({"id": "loop", "command": "tabs.list", "args": {}})
assert result["success"] is False
assert "paging loop exceeded" in result["error"]
def test_collect_paged_browser_command_invalid_items(monkeypatch):
"""If items is not a list the command returns an error dict."""
monkeypatch.setattr(
native_host, "_send_browser_command",
lambda cmd: {
"id": cmd["id"],
"success": True,
"data": {"__browserCliPage": True, "items": "not-a-list", "total": 1, "nextOffset": None},
},
)
result = native_host._collect_paged_browser_command({"id": "bad", "command": "tabs.list", "args": {}})
assert result["success"] is False
assert "invalid paged response" in result["error"]
# ---------------------------------------------------------------------------
# _resolve_profile_alias
# ---------------------------------------------------------------------------
def test_resolve_profile_alias_uses_hello_alias():
alias = native_host._resolve_profile_alias({"type": "hello", "alias": "brave-work"})
assert alias == "brave-work"
def test_resolve_profile_alias_no_hello_returns_uuid():
alias = native_host._resolve_profile_alias(None)
import uuid
uuid.UUID(alias) # raises ValueError if not a valid UUID
def test_resolve_profile_alias_default_alias_returns_uuid():
from browser_cli.platform import DEFAULT_ALIAS
alias = native_host._resolve_profile_alias({"type": "hello", "alias": DEFAULT_ALIAS})
import uuid
uuid.UUID(alias)
def test_resolve_profile_alias_non_hello_type_returns_uuid():
alias = native_host._resolve_profile_alias({"type": "bye", "alias": "some"})
import uuid
uuid.UUID(alias)
+39
View File
@@ -0,0 +1,39 @@
"""Integration tests for page.info command — require a live browser."""
def test_page_info_returns_required_fields(browser, http_tab):
"""page.info returns title, url, readyState and lang."""
browser("tabs.active", {"tabId": http_tab["id"]})
info = browser("page.info")
assert isinstance(info, dict)
assert "title" in info
assert "url" in info
assert "readyState" in info
assert "lang" in info
def test_page_info_url_matches_active_tab(browser, http_tab):
"""URL reported by page.info matches the tab URL in tabs.list."""
browser("tabs.active", {"tabId": http_tab["id"]})
info = browser("page.info")
assert info is not None
# Active tab URL should match (allow for trailing slash difference)
assert "example.com" in info.get("url", "")
def test_page_info_ready_state_complete(browser, http_tab):
"""A fully loaded page reports readyState == 'complete'."""
browser("tabs.active", {"tabId": http_tab["id"]})
info = browser("page.info")
assert info.get("readyState") == "complete"
def test_page_info_title_non_empty(browser, http_tab):
"""example.com has a non-empty title."""
browser("tabs.active", {"tabId": http_tab["id"]})
info = browser("page.info")
assert isinstance(info.get("title"), str)
assert len(info["title"]) > 0
def test_page_info_meta_is_dict(browser, http_tab):
"""meta field is a dict (may be empty for simple pages)."""
browser("tabs.active", {"tabId": http_tab["id"]})
info = browser("page.info")
meta = info.get("meta")
assert meta is None or isinstance(meta, dict)
+72
View File
@@ -0,0 +1,72 @@
"""Integration tests for perf.* commands — require a live browser."""
import time
_VALID_PROFILES = {"auto", "normal", "gentle", "ultra"}
def test_perf_status_returns_profile_field(browser):
"""perf.status returns a dict with a performanceProfile field."""
result = browser("perf.status")
assert isinstance(result, dict)
assert "performanceProfile" in result
assert result["performanceProfile"] in _VALID_PROFILES
def test_perf_status_has_throttle(browser):
"""perf.status includes a throttle dict with batchSize and pauseMs."""
result = browser("perf.status")
throttle = result.get("throttle")
assert isinstance(throttle, dict)
assert "batchSize" in throttle
assert "pauseMs" in throttle
assert isinstance(throttle["batchSize"], int)
assert isinstance(throttle["pauseMs"], int)
assert throttle["batchSize"] > 0
assert throttle["pauseMs"] >= 0
def test_perf_status_has_audible_field(browser):
"""perf.status contains an audible boolean."""
result = browser("perf.status")
assert "audible" in result
assert isinstance(result["audible"], bool)
def test_perf_status_has_jobs_field(browser):
"""perf.status contains a jobs list (may be empty)."""
result = browser("perf.status")
assert "jobs" in result
assert isinstance(result["jobs"], list)
def test_perf_set_profile_returns_profile(browser):
"""perf.set_profile responds with the applied profile."""
# Save current
current = browser("perf.status").get("performanceProfile", "auto")
target = "normal" if current != "normal" else "gentle"
try:
result = browser("perf.set_profile", {"profile": target})
assert isinstance(result, dict)
assert result.get("performanceProfile") == target
finally:
browser("perf.set_profile", {"profile": current})
def test_perf_set_profile_updates_status(browser):
"""After set_profile the status command reflects the new profile."""
current = browser("perf.status").get("performanceProfile", "auto")
target = "gentle" if current != "gentle" else "normal"
try:
browser("perf.set_profile", {"profile": target})
status = browser("perf.status")
assert status.get("performanceProfile") == target
finally:
browser("perf.set_profile", {"profile": current})
def test_perf_profile_roundtrip_all_values(browser):
"""Every valid profile can be set and read back without error."""
original = browser("perf.status").get("performanceProfile", "auto")
try:
for profile in sorted(_VALID_PROFILES):
result = browser("perf.set_profile", {"profile": profile})
assert result.get("performanceProfile") == profile
status = browser("perf.status")
assert status.get("performanceProfile") == profile
finally:
browser("perf.set_profile", {"profile": original})
+54
View File
@@ -0,0 +1,54 @@
"""Integration tests for storage.get / storage.set — require a live browser."""
import time
_KEY_PREFIX = "__pytest_storage_"
def test_storage_set_and_get_roundtrip(browser, http_tab):
"""Set a localStorage key then read it back."""
browser("tabs.active", {"tabId": http_tab["id"]})
key = _KEY_PREFIX + "roundtrip"
value = "hello-storage"
browser("storage.set", {"key": key, "value": value, "type": "local", "tabId": http_tab["id"]})
result = browser("storage.get", {"key": key, "type": "local", "tabId": http_tab["id"]})
assert result == value
def test_storage_get_nonexistent_returns_null(browser, http_tab):
"""Getting a key that was never set returns None."""
browser("tabs.active", {"tabId": http_tab["id"]})
result = browser("storage.get", {
"key": _KEY_PREFIX + "no_such_key_zzz",
"type": "local",
"tabId": http_tab["id"],
})
assert result is None
def test_storage_get_all_returns_dict(browser, http_tab):
"""Calling storage.get without a key dumps all entries as a dict."""
browser("tabs.active", {"tabId": http_tab["id"]})
# Plant at least one key so the dump isn't trivially empty
browser("storage.set", {"key": _KEY_PREFIX + "dump", "value": "present", "type": "local", "tabId": http_tab["id"]})
result = browser("storage.get", {"key": None, "type": "local", "tabId": http_tab["id"]})
assert isinstance(result, dict)
assert _KEY_PREFIX + "dump" in result
def test_storage_session_type_roundtrip(browser, http_tab):
"""sessionStorage works the same way as localStorage."""
browser("tabs.active", {"tabId": http_tab["id"]})
key = _KEY_PREFIX + "session"
value = "ses-val"
browser("storage.set", {"key": key, "value": value, "type": "session", "tabId": http_tab["id"]})
result = browser("storage.get", {"key": key, "type": "session", "tabId": http_tab["id"]})
assert result == value
def test_storage_overwrite_updates_value(browser, http_tab):
"""Setting the same key twice reflects the latest value."""
browser("tabs.active", {"tabId": http_tab["id"]})
key = _KEY_PREFIX + "overwrite"
browser("storage.set", {"key": key, "value": "v1", "type": "local", "tabId": http_tab["id"]})
browser("storage.set", {"key": key, "value": "v2", "type": "local", "tabId": http_tab["id"]})
result = browser("storage.get", {"key": key, "type": "local", "tabId": http_tab["id"]})
assert result == "v2"