add moveing of tabs and groups, multi browser support, auto complite into terminal, extract html and adding testing
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Shared pytest fixtures for browser-cli integration tests.
|
||||
|
||||
Tests that require a live browser connection use the `browser` fixture.
|
||||
They are automatically skipped if the native host socket is not reachable.
|
||||
"""
|
||||
import pytest
|
||||
from browser_cli.client import send_command, BrowserNotConnected
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def browser():
|
||||
"""Returns a connected send_command callable, or skips the test."""
|
||||
try:
|
||||
send_command("tabs.list")
|
||||
except BrowserNotConnected:
|
||||
pytest.skip("Browser not connected — start Brave/Chrome with the extension loaded")
|
||||
return send_command
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def http_tab(browser):
|
||||
"""Ensures at least one http/https tab is open; returns its tab info."""
|
||||
tabs = browser("tabs.list")
|
||||
http_tab = next(
|
||||
(t for t in tabs if t.get("url", "").startswith("http")), None
|
||||
)
|
||||
if http_tab is None:
|
||||
pytest.skip("No http/https tab open — open a web page first")
|
||||
return http_tab
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Tests for dom.* commands (require an http/https active tab)."""
|
||||
import pytest
|
||||
from browser_cli.client import send_command
|
||||
|
||||
|
||||
def test_dom_query_body(browser, http_tab):
|
||||
browser("tabs.active", {"tabId": http_tab["id"]})
|
||||
elements = browser("dom.query", {"selector": "body"})
|
||||
assert isinstance(elements, list)
|
||||
assert len(elements) == 1
|
||||
assert elements[0]["tag"] == "body"
|
||||
|
||||
|
||||
def test_dom_query_multiple(browser, http_tab):
|
||||
browser("tabs.active", {"tabId": http_tab["id"]})
|
||||
# Every HTML page has at least one element
|
||||
elements = browser("dom.query", {"selector": "*"})
|
||||
assert isinstance(elements, list)
|
||||
assert len(elements) > 1
|
||||
|
||||
|
||||
def test_dom_query_no_match(browser, http_tab):
|
||||
browser("tabs.active", {"tabId": http_tab["id"]})
|
||||
elements = browser("dom.query", {"selector": "#zzz_no_such_element_zzz"})
|
||||
assert isinstance(elements, list)
|
||||
assert len(elements) == 0
|
||||
|
||||
|
||||
def test_dom_exists_true(browser, http_tab):
|
||||
browser("tabs.active", {"tabId": http_tab["id"]})
|
||||
result = browser("dom.exists", {"selector": "html"})
|
||||
assert result is True
|
||||
|
||||
|
||||
def test_dom_exists_false(browser, http_tab):
|
||||
browser("tabs.active", {"tabId": http_tab["id"]})
|
||||
result = browser("dom.exists", {"selector": "#zzz_no_such_element_zzz"})
|
||||
assert result is False
|
||||
|
||||
|
||||
def test_dom_text_body(browser, http_tab):
|
||||
browser("tabs.active", {"tabId": http_tab["id"]})
|
||||
texts = browser("dom.text", {"selector": "body"})
|
||||
assert isinstance(texts, list)
|
||||
assert len(texts) > 0
|
||||
assert isinstance(texts[0], str)
|
||||
assert len(texts[0]) > 0
|
||||
|
||||
|
||||
def test_dom_attr_returns_list(browser, http_tab):
|
||||
browser("tabs.active", {"tabId": http_tab["id"]})
|
||||
# Get href of all anchor tags — page may or may not have any
|
||||
hrefs = browser("dom.attr", {"selector": "a", "attr": "href"})
|
||||
assert isinstance(hrefs, list)
|
||||
|
||||
|
||||
def test_dom_attr_html_lang(browser, http_tab):
|
||||
browser("tabs.active", {"tabId": http_tab["id"]})
|
||||
langs = browser("dom.attr", {"selector": "html", "attr": "lang"})
|
||||
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
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Tests for extract.* commands (require an http/https active tab)."""
|
||||
import pytest
|
||||
from browser_cli.client import send_command
|
||||
|
||||
|
||||
def test_extract_links(browser, http_tab):
|
||||
browser("tabs.active", {"tabId": http_tab["id"]})
|
||||
links = browser("extract.links")
|
||||
assert isinstance(links, list)
|
||||
for lnk in links:
|
||||
assert "href" in lnk
|
||||
assert "text" in lnk
|
||||
|
||||
|
||||
def test_extract_images(browser, http_tab):
|
||||
browser("tabs.active", {"tabId": http_tab["id"]})
|
||||
images = browser("extract.images")
|
||||
assert isinstance(images, list)
|
||||
for img in images:
|
||||
assert "src" in img
|
||||
assert img["src"] != ""
|
||||
|
||||
|
||||
def test_extract_text(browser, http_tab):
|
||||
browser("tabs.active", {"tabId": http_tab["id"]})
|
||||
text = browser("extract.text")
|
||||
assert isinstance(text, str)
|
||||
assert len(text) > 0
|
||||
|
||||
|
||||
def test_extract_html(browser, http_tab):
|
||||
browser("tabs.active", {"tabId": http_tab["id"]})
|
||||
html = browser("extract.html")
|
||||
assert isinstance(html, str)
|
||||
assert "<" in html
|
||||
|
||||
|
||||
def test_dom_exists(browser, http_tab):
|
||||
browser("tabs.active", {"tabId": http_tab["id"]})
|
||||
result = browser("dom.exists", {"selector": "body"})
|
||||
assert result is True
|
||||
|
||||
|
||||
def test_dom_query(browser, http_tab):
|
||||
browser("tabs.active", {"tabId": http_tab["id"]})
|
||||
elements = browser("dom.query", {"selector": "body"})
|
||||
assert isinstance(elements, list)
|
||||
assert len(elements) > 0
|
||||
assert elements[0].get("tag") == "body"
|
||||
@@ -0,0 +1,133 @@
|
||||
"""Tests for group.* commands."""
|
||||
import pytest
|
||||
from browser_cli.client import send_command
|
||||
|
||||
|
||||
def test_group_list(browser):
|
||||
groups = browser("group.list")
|
||||
assert isinstance(groups, list)
|
||||
|
||||
|
||||
def test_group_count_is_int(browser):
|
||||
count = browser("group.count")
|
||||
assert isinstance(count, int)
|
||||
assert count >= 0
|
||||
|
||||
|
||||
def test_group_count_matches_list(browser):
|
||||
groups = browser("group.list")
|
||||
count = browser("group.count")
|
||||
assert count == len(groups)
|
||||
|
||||
|
||||
def test_group_create_and_close(browser):
|
||||
result = browser("group.open", {"name": "__test_group__"})
|
||||
assert isinstance(result, dict)
|
||||
gid = result["id"]
|
||||
|
||||
# Verify it appears in the list
|
||||
groups = browser("group.list")
|
||||
assert any(g["id"] == gid for g in groups)
|
||||
|
||||
# Get tabs inside so we can clean them up after ungrouping
|
||||
tabs_in_group = browser("group.tabs", {"groupId": gid})
|
||||
|
||||
# Close (ungroup) the group
|
||||
browser("group.close", {"groupId": gid})
|
||||
|
||||
# Group should be gone
|
||||
groups_after = browser("group.list")
|
||||
assert gid not in [g["id"] for g in groups_after]
|
||||
|
||||
# Clean up the ungrouped tabs
|
||||
for t in tabs_in_group:
|
||||
try:
|
||||
browser("tabs.close", {"tabId": t["id"]})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def test_group_create_has_title(browser):
|
||||
result = browser("group.open", {"name": "__titled_group__"})
|
||||
gid = result["id"]
|
||||
tabs_in_group = browser("group.tabs", {"groupId": gid})
|
||||
|
||||
try:
|
||||
groups = browser("group.list")
|
||||
match = next((g for g in groups if g["id"] == gid), None)
|
||||
assert match is not None
|
||||
assert match.get("title") == "__titled_group__"
|
||||
assert match.get("tabCount", 0) >= 1
|
||||
finally:
|
||||
browser("group.close", {"groupId": gid})
|
||||
for t in tabs_in_group:
|
||||
try:
|
||||
browser("tabs.close", {"tabId": t["id"]})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def test_group_query(browser):
|
||||
result = browser("group.open", {"name": "__query_test__"})
|
||||
gid = result["id"]
|
||||
tabs_in_group = browser("group.tabs", {"groupId": gid})
|
||||
|
||||
try:
|
||||
found = browser("group.query", {"search": "__query_test__"})
|
||||
assert isinstance(found, list)
|
||||
assert any(g["id"] == gid for g in found)
|
||||
finally:
|
||||
browser("group.close", {"groupId": gid})
|
||||
for t in tabs_in_group:
|
||||
try:
|
||||
browser("tabs.close", {"tabId": t["id"]})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def test_group_query_no_match(browser):
|
||||
result = browser("group.query", {"search": "zzz_no_such_group_zzz"})
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 0
|
||||
|
||||
|
||||
def test_group_add_tab(browser):
|
||||
grp = browser("group.open", {"name": "__add_tab_test__"})
|
||||
gid = grp["id"]
|
||||
initial_tabs = browser("group.tabs", {"groupId": gid})
|
||||
|
||||
try:
|
||||
tab_result = browser("group.add_tab", {"group": str(gid), "url": "https://example.com"})
|
||||
assert isinstance(tab_result, dict)
|
||||
new_tab_id = tab_result["tabId"]
|
||||
|
||||
tabs = browser("group.tabs", {"groupId": gid})
|
||||
assert any(t["id"] == new_tab_id for t in tabs)
|
||||
finally:
|
||||
all_tabs = browser("group.tabs", {"groupId": gid})
|
||||
browser("group.close", {"groupId": gid})
|
||||
for t in all_tabs + initial_tabs:
|
||||
try:
|
||||
browser("tabs.close", {"tabId": t["id"]})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def test_group_tabs_returns_list(browser):
|
||||
grp = browser("group.open", {"name": "__tabs_list_test__"})
|
||||
gid = grp["id"]
|
||||
tabs_in_group = browser("group.tabs", {"groupId": gid})
|
||||
|
||||
try:
|
||||
assert isinstance(tabs_in_group, list)
|
||||
assert len(tabs_in_group) >= 1
|
||||
for t in tabs_in_group:
|
||||
assert "id" in t
|
||||
assert "url" in t
|
||||
finally:
|
||||
browser("group.close", {"groupId": gid})
|
||||
for t in tabs_in_group:
|
||||
try:
|
||||
browser("tabs.close", {"tabId": t["id"]})
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Tests for navigate.* commands."""
|
||||
import pytest
|
||||
from browser_cli.client import send_command
|
||||
|
||||
|
||||
def test_nav_open_and_close(browser):
|
||||
"""Open a tab, verify it appears in the list, then close it."""
|
||||
result = browser("navigate.open", {"url": "https://example.com", "background": True})
|
||||
assert "id" in result
|
||||
new_id = result["id"]
|
||||
|
||||
tabs = browser("tabs.list")
|
||||
ids = [t["id"] for t in tabs]
|
||||
assert new_id in ids
|
||||
|
||||
# Clean up
|
||||
browser("tabs.close", {"tabId": new_id})
|
||||
tabs_after = browser("tabs.list")
|
||||
assert new_id not in [t["id"] for t in tabs_after]
|
||||
|
||||
|
||||
def test_nav_focus_by_pattern(browser):
|
||||
# Open a known URL in background first
|
||||
result = browser("navigate.open", {"url": "https://example.com", "background": True})
|
||||
tab_id = result["id"]
|
||||
|
||||
focus_result = browser("navigate.focus", {"pattern": "example.com"})
|
||||
assert focus_result is not None
|
||||
assert "example.com" in (focus_result.get("url") or "")
|
||||
|
||||
browser("tabs.close", {"tabId": tab_id})
|
||||
|
||||
|
||||
def test_nav_focus_by_tab_id(browser):
|
||||
result = browser("navigate.open", {"url": "https://example.com", "background": True})
|
||||
tab_id = result["id"]
|
||||
|
||||
focus_result = browser("navigate.focus", {"pattern": str(tab_id)})
|
||||
assert focus_result is not None
|
||||
assert focus_result.get("id") == tab_id
|
||||
|
||||
browser("tabs.close", {"tabId": tab_id})
|
||||
|
||||
|
||||
def test_nav_reload(browser):
|
||||
result = browser("navigate.open", {"url": "https://example.com", "background": True})
|
||||
tab_id = result["id"]
|
||||
|
||||
try:
|
||||
reload_result = browser("navigate.reload", {"tabId": tab_id})
|
||||
assert reload_result is None or isinstance(reload_result, dict)
|
||||
finally:
|
||||
browser("tabs.close", {"tabId": tab_id})
|
||||
|
||||
|
||||
def test_nav_hard_reload(browser):
|
||||
result = browser("navigate.open", {"url": "https://example.com", "background": True})
|
||||
tab_id = result["id"]
|
||||
|
||||
try:
|
||||
result = browser("navigate.hard_reload", {"tabId": tab_id})
|
||||
assert result is None or isinstance(result, dict)
|
||||
finally:
|
||||
try:
|
||||
browser("tabs.close", {"tabId": tab_id})
|
||||
except Exception:
|
||||
pass # tab ID may change after hard reload
|
||||
|
||||
|
||||
def test_nav_open_in_background(browser):
|
||||
"""Tab opened with background=True should not be the active tab."""
|
||||
active_before = next(
|
||||
t for t in browser("tabs.list") if t.get("active")
|
||||
)
|
||||
result = browser("navigate.open", {"url": "https://example.com", "background": True})
|
||||
new_id = result["id"]
|
||||
|
||||
try:
|
||||
tabs = browser("tabs.list")
|
||||
new_tab = next(t for t in tabs if t["id"] == new_id)
|
||||
assert not new_tab.get("active"), "background tab should not be active"
|
||||
finally:
|
||||
browser("tabs.close", {"tabId": new_id})
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Tests for session.* commands."""
|
||||
import pytest
|
||||
from browser_cli.client import send_command
|
||||
|
||||
SESSION_NAME = "_pytest_session"
|
||||
|
||||
|
||||
def test_session_save_and_list(browser):
|
||||
browser("session.save", {"name": SESSION_NAME})
|
||||
|
||||
sessions = browser("session.list")
|
||||
assert isinstance(sessions, list)
|
||||
names = [s["name"] for s in sessions]
|
||||
assert SESSION_NAME in names
|
||||
|
||||
|
||||
def test_session_list_has_tab_count(browser):
|
||||
sessions = browser("session.list")
|
||||
for s in sessions:
|
||||
assert "name" in s
|
||||
assert "tabs" in s
|
||||
assert isinstance(s["tabs"], int)
|
||||
|
||||
|
||||
def test_session_diff(browser):
|
||||
browser("session.save", {"name": SESSION_NAME + "_a"})
|
||||
browser("session.save", {"name": SESSION_NAME + "_b"})
|
||||
diff = browser("session.diff", {"nameA": SESSION_NAME + "_a", "nameB": SESSION_NAME + "_b"})
|
||||
assert "added" in diff
|
||||
assert "removed" in diff
|
||||
|
||||
|
||||
def test_session_remove(browser):
|
||||
browser("session.save", {"name": SESSION_NAME + "_remove"})
|
||||
browser("session.remove", {"name": SESSION_NAME + "_remove"})
|
||||
sessions = browser("session.list")
|
||||
names = [s["name"] for s in sessions]
|
||||
assert SESSION_NAME + "_remove" not in names
|
||||
|
||||
|
||||
def teardown_module(module):
|
||||
"""Clean up test sessions after all tests run."""
|
||||
for name in [SESSION_NAME, SESSION_NAME + "_a", SESSION_NAME + "_b"]:
|
||||
try:
|
||||
send_command("session.remove", {"name": name})
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Tests for tabs.* commands."""
|
||||
import pytest
|
||||
from browser_cli.client import send_command
|
||||
|
||||
|
||||
def test_tabs_list(browser):
|
||||
tabs = browser("tabs.list")
|
||||
assert isinstance(tabs, list)
|
||||
assert len(tabs) > 0
|
||||
first = tabs[0]
|
||||
assert "id" in first
|
||||
assert "windowId" in first
|
||||
assert "url" in first
|
||||
assert "title" in first
|
||||
|
||||
|
||||
def test_tabs_count(browser):
|
||||
count = browser("tabs.count", {})
|
||||
tabs = browser("tabs.list")
|
||||
assert count == len(tabs)
|
||||
|
||||
|
||||
def test_tabs_count_with_pattern(browser):
|
||||
count = browser("tabs.count", {"pattern": "http"})
|
||||
assert isinstance(count, int)
|
||||
assert count >= 0
|
||||
|
||||
|
||||
def test_tabs_filter(browser):
|
||||
result = browser("tabs.filter", {"pattern": "http"})
|
||||
assert isinstance(result, list)
|
||||
for tab in result:
|
||||
assert "http" in tab.get("url", "")
|
||||
|
||||
|
||||
def test_tabs_query(browser):
|
||||
result = browser("tabs.query", {"search": "a"})
|
||||
assert isinstance(result, list)
|
||||
|
||||
|
||||
def test_tabs_active_exists(browser):
|
||||
tabs = browser("tabs.list")
|
||||
active = [t for t in tabs if t.get("active")]
|
||||
assert len(active) >= 1, "Expected at least one active tab"
|
||||
|
||||
|
||||
def test_tabs_html(browser, http_tab):
|
||||
html = browser("tabs.html", {"tabId": http_tab["id"]})
|
||||
assert isinstance(html, str)
|
||||
assert len(html) > 0
|
||||
assert "<html" in html.lower() or "<!doctype" in html.lower()
|
||||
|
||||
|
||||
def test_tabs_close_by_id(browser):
|
||||
result = browser("navigate.open", {"url": "https://example.com", "background": True})
|
||||
tab_id = result["id"]
|
||||
|
||||
browser("tabs.close", {"tabId": tab_id})
|
||||
|
||||
tabs = browser("tabs.list")
|
||||
assert tab_id not in [t["id"] for t in tabs]
|
||||
|
||||
|
||||
def test_tabs_dedupe(browser):
|
||||
# Open the same URL twice
|
||||
r1 = browser("navigate.open", {"url": "https://example.com", "background": True})
|
||||
r2 = browser("navigate.open", {"url": "https://example.com", "background": True})
|
||||
id1, id2 = r1["id"], r2["id"]
|
||||
|
||||
try:
|
||||
result = browser("tabs.dedupe")
|
||||
assert isinstance(result, dict)
|
||||
assert result.get("closed", 0) >= 0
|
||||
# At least one of the two duplicates should be gone
|
||||
remaining = browser("tabs.list")
|
||||
remaining_ids = {t["id"] for t in remaining}
|
||||
assert not (id1 in remaining_ids and id2 in remaining_ids), \
|
||||
"Both duplicate tabs still open after dedupe"
|
||||
finally:
|
||||
for tid in (id1, id2):
|
||||
try:
|
||||
browser("tabs.close", {"tabId": tid})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def test_tabs_sort(browser):
|
||||
result = browser("tabs.sort", {"by": "domain"})
|
||||
# No error and at least returns something (None or dict)
|
||||
assert result is None or isinstance(result, dict)
|
||||
|
||||
|
||||
def test_tabs_move_forward(browser):
|
||||
r1 = browser("navigate.open", {"url": "https://example.com", "background": True})
|
||||
r2 = browser("navigate.open", {"url": "https://example.com", "background": True})
|
||||
id1, id2 = r1["id"], r2["id"]
|
||||
|
||||
try:
|
||||
# Move id1 forward — just verify no error is raised
|
||||
browser("tabs.move", {"tabId": id1, "forward": True})
|
||||
finally:
|
||||
browser("tabs.close", {"tabId": id1})
|
||||
browser("tabs.close", {"tabId": id2})
|
||||
|
||||
|
||||
def test_tabs_merge_windows_no_crash(browser):
|
||||
result = browser("tabs.merge_windows")
|
||||
assert isinstance(result, dict)
|
||||
assert "moved" in result
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Tests for windows.* commands."""
|
||||
import pytest
|
||||
from browser_cli.client import send_command
|
||||
|
||||
|
||||
def test_windows_list(browser):
|
||||
windows = browser("windows.list")
|
||||
assert isinstance(windows, list)
|
||||
assert len(windows) >= 1
|
||||
|
||||
|
||||
def test_windows_each_has_required_fields(browser):
|
||||
windows = browser("windows.list")
|
||||
for w in windows:
|
||||
assert "id" in w
|
||||
assert isinstance(w["id"], int)
|
||||
assert "tabCount" in w
|
||||
assert isinstance(w["tabCount"], int)
|
||||
|
||||
|
||||
def test_windows_has_state(browser):
|
||||
windows = browser("windows.list")
|
||||
# Every window should report a state (normal, minimized, maximized, fullscreen)
|
||||
for w in windows:
|
||||
assert "state" in w
|
||||
|
||||
|
||||
def test_windows_open_and_close(browser):
|
||||
result = browser("windows.open", {})
|
||||
assert isinstance(result, dict)
|
||||
new_id = result["id"]
|
||||
|
||||
windows = browser("windows.list")
|
||||
assert any(w["id"] == new_id for w in windows)
|
||||
|
||||
browser("windows.close", {"windowId": new_id})
|
||||
|
||||
windows_after = browser("windows.list")
|
||||
assert new_id not in [w["id"] for w in windows_after]
|
||||
|
||||
|
||||
def test_windows_tab_count_positive(browser):
|
||||
windows = browser("windows.list")
|
||||
for w in windows:
|
||||
assert w["tabCount"] >= 0
|
||||
|
||||
|
||||
def test_windows_rename(browser):
|
||||
result = browser("windows.open", {})
|
||||
new_id = result["id"]
|
||||
|
||||
try:
|
||||
rename_result = browser("windows.rename", {"windowId": new_id, "name": "__test_alias__"})
|
||||
assert isinstance(rename_result, dict)
|
||||
|
||||
windows = browser("windows.list")
|
||||
match = next((w for w in windows if w["id"] == new_id), None)
|
||||
assert match is not None
|
||||
assert match.get("alias") == "__test_alias__"
|
||||
finally:
|
||||
browser("windows.close", {"windowId": new_id})
|
||||
Reference in New Issue
Block a user