fd5447cbb9
Testing / remote-protocol-compat (0.9.3) (push) Successful in 42s
Testing / remote-protocol-compat (0.9.5) (push) Successful in 44s
Package Extension / package-extension (push) Successful in 43s
Build & Publish Package / publish (push) Successful in 43s
Testing / test (push) Successful in 45s
Restructure the Python API and internals around composable namespaces and a standalone transport/endpoint layer. Bump to 0.12.0. Python API: - Replace flat methods (b.tabs_list(), b.group_list()) with namespaces: b.nav, b.tabs, b.groups, b.windows, b.dom, b.extract, b.page, b.storage, b.cookies, b.session, b.perf, b.extension. - Shrink browser_cli/__init__.py to a thin composition root; move all behaviour into browser_cli/sdk/ (one module per namespace + factories, base, routing). Internals: - Add browser_cli/transport.py and remote_transport.py to isolate IPC from command logic; client.py now delegates instead of owning transport. - Add browser_cli/endpoints.py for endpoint resolution and browser_cli/errors.py for shared error types. - Extract markdown rendering into browser_cli/markdown.py (out of extract). - Add USER_AGENT to version_manager. Tooling & tests: - Add justfile with common dev tasks. - Update CLI commands and demo to the namespaced API. - Rework tests for the new layout; add test_transport.py and test_refactor_boundaries.py to lock in module boundaries. BREAKING CHANGE: flat API methods are removed in favour of namespaces (e.g. b.tabs_list() -> b.tabs.list(), b.group_list() -> b.groups.list()).
124 lines
5.1 KiB
Python
124 lines
5.1 KiB
Python
"""Guards for the module boundaries introduced by the modularity refactor.
|
|
|
|
Three things must hold so the split stays invisible to callers:
|
|
* the new leaf modules own their logic and behave correctly,
|
|
* ``browser_cli.client`` still re-exports the names that moved out of it, and
|
|
* ``BrowserCLI`` still exposes the factory/routing helpers (now mixins).
|
|
"""
|
|
from unittest.mock import patch
|
|
|
|
import browser_cli
|
|
from browser_cli import BrowserCLI, BrowserCounts
|
|
|
|
# ── endpoints module ────────────────────────────────────────────────────────
|
|
|
|
class TestEndpoints:
|
|
def test_normalize_strips_default_https_port_for_domains(self):
|
|
from browser_cli.endpoints import _normalize_endpoint
|
|
|
|
assert _normalize_endpoint("example.com:443") == "example.com"
|
|
assert _normalize_endpoint("example.com:8765") == "example.com:8765"
|
|
assert _normalize_endpoint("192.168.1.10:443") == "192.168.1.10:443"
|
|
|
|
def test_resolve_connect_endpoint_defaults_domain_to_443(self):
|
|
from browser_cli.endpoints import _resolve_connect_endpoint
|
|
|
|
assert _resolve_connect_endpoint("example.com") == "example.com:443"
|
|
assert _resolve_connect_endpoint("host:9000") == "host:9000"
|
|
|
|
def test_resolve_connect_endpoint_rejects_bare_non_domain(self):
|
|
from browser_cli.errors import BrowserNotConnected
|
|
from browser_cli.endpoints import _resolve_connect_endpoint
|
|
|
|
try:
|
|
_resolve_connect_endpoint("localhost")
|
|
except BrowserNotConnected:
|
|
pass
|
|
else:
|
|
raise AssertionError("expected BrowserNotConnected for bare 'localhost'")
|
|
|
|
def test_looks_like_domain(self):
|
|
from browser_cli.endpoints import _looks_like_domain
|
|
|
|
assert _looks_like_domain("example.com") is True
|
|
assert _looks_like_domain("localhost") is False
|
|
assert _looks_like_domain("127.0.0.1") is False
|
|
|
|
# ── markdown module ─────────────────────────────────────────────────────────
|
|
|
|
class TestRenderMarkdown:
|
|
def test_html_payload_is_converted(self):
|
|
from browser_cli.markdown import render_markdown
|
|
|
|
assert render_markdown("<h1>Title</h1>") == "# Title"
|
|
|
|
def test_markdown_payload_is_cleaned_not_html_parsed(self):
|
|
from browser_cli.markdown import render_markdown
|
|
|
|
assert render_markdown(r"hello\_world \- item") == "hello_world - item"
|
|
|
|
def test_none_and_empty_are_safe(self):
|
|
from browser_cli.markdown import render_markdown
|
|
|
|
assert render_markdown(None) == ""
|
|
assert render_markdown("") == ""
|
|
|
|
# ── backward-compatible re-exports ──────────────────────────────────────────
|
|
|
|
class TestReExports:
|
|
def test_client_still_exposes_moved_names(self):
|
|
from browser_cli import client
|
|
|
|
# moved to endpoints / remote_transport / _errors but still reachable here
|
|
for name in (
|
|
"BrowserNotConnected",
|
|
"_normalize_endpoint",
|
|
"_resolve_connect_endpoint",
|
|
"display_browser_name",
|
|
"_remote_display_name",
|
|
"_send_remote",
|
|
"_recv_all",
|
|
"_recv_exact",
|
|
):
|
|
assert hasattr(client, name), f"browser_cli.client.{name} missing after refactor"
|
|
|
|
def test_browsercounts_reexported_from_package(self):
|
|
# BrowserCounts moved to models but is still part of the public API.
|
|
from browser_cli.models import BrowserCounts as ModelsBrowserCounts
|
|
|
|
assert BrowserCounts is ModelsBrowserCounts
|
|
|
|
def test_patching_client_send_remote_still_intercepts(self):
|
|
# send_command resolves _send_remote as a browser_cli.client global, so
|
|
# patching there must still take effect after the move to remote_transport.
|
|
with patch("browser_cli.client._send_remote", return_value=None) as fake:
|
|
assert browser_cli.client._send_remote is fake
|
|
|
|
# ── BrowserCLI mixin composition ─────────────────────────────────────────────
|
|
|
|
class TestMixinComposition:
|
|
def test_factory_builds_bound_tab(self):
|
|
b = BrowserCLI()
|
|
tab = b._make_tab({"id": 7, "windowId": 1, "title": "t", "url": "u"})
|
|
assert tab.id == 7
|
|
assert tab._browser is b
|
|
|
|
def test_factory_tab_for_remote_target_binds_sibling_client(self):
|
|
b = BrowserCLI()
|
|
|
|
class _Target:
|
|
profile = "work"
|
|
display_name = "host:work"
|
|
remote = "host:8765"
|
|
|
|
tab = b._make_tab_for({"id": 1, "windowId": 0}, _Target())
|
|
assert tab.browser == "host:work"
|
|
assert isinstance(tab._browser, BrowserCLI)
|
|
assert tab._browser is not b
|
|
assert tab._browser.browser == "work"
|
|
|
|
def test_field_helper_handles_non_dict(self):
|
|
b = BrowserCLI()
|
|
assert b._field({"tabId": 5}, "tabId") == 5
|
|
assert b._field("not-a-dict", "tabId", fallback=9) == 9
|