feat(read): target any tab for page info and extraction
Testing / test (push) Successful in 40s
Testing / remote-protocol-compat (0.16.0) (push) Successful in 31s
Testing / remote-protocol-compat (0.15.0) (push) Successful in 39s

- Add --tab option to all extract commands and page info
- Thread tab_id through the SDK extract and page namespaces
- Route page.info with a tab_id to tabs.status for cross-tab metadata
- Accept tab_id in the MCP page_info, extract_text, extract_markdown tools
- Forward tabId in the extension page.info and extract.html handlers
- Keep the active tab as default when no tab is given

- Cover tab-scoped reads in API, CLI, and MCP tests
- Bump package and extension version to 0.16.7
- Refresh uv.lock with current dependency versions
This commit is contained in:
2026-08-28 11:19:02 +02:00
parent 6352d9994e
commit 914508e2db
11 changed files with 522 additions and 410 deletions
+14 -3
View File
@@ -299,22 +299,28 @@ class TestExtract:
result = b.extract.markdown()
assert result == "# Title"
mock_send.assert_called_once_with("extract.markdown", {"selector": None}, profile=None, remote=None, key=None)
mock_send.assert_called_once_with("extract.markdown", {"selector": None, "tabId": None}, profile=None, remote=None, key=None)
def test_extract_markdown_selector(self, b, mock_send):
mock_send.return_value = "## Post"
assert b.extract.markdown("article") == "## Post"
mock_send.assert_called_once_with("extract.markdown", {"selector": "article"}, profile=None, remote=None, key=None)
mock_send.assert_called_once_with("extract.markdown", {"selector": "article", "tabId": None}, profile=None, remote=None, key=None)
def test_extract_links(self, b, mock_send):
mock_send.return_value = [{"href": "https://x"}]
assert b.extract.links() == [{"href": "https://x"}]
mock_send.assert_called_once_with("extract.links", {}, profile=None, remote=None, key=None)
mock_send.assert_called_once_with("extract.links", {"tabId": None}, profile=None, remote=None, key=None)
def test_extract_text_none(self, b, mock_send):
mock_send.return_value = None
assert b.extract.text() == ""
mock_send.assert_called_once_with("extract.text", {"tabId": None}, profile=None, remote=None, key=None)
def test_extract_markdown_tab_id(self, b, mock_send):
mock_send.return_value = "# Specific"
assert b.extract.markdown(tab_id=42) == "# Specific"
mock_send.assert_called_once_with("extract.markdown", {"selector": None, "tabId": 42}, profile=None, remote=None, key=None)
# ── Tabs ──────────────────────────────────────────────────────────────────────
@@ -869,6 +875,11 @@ class TestPageStorageCookies:
assert b.page.info() == {"title": "X"}
mock_send.assert_called_once_with("page.info", {}, profile=None, remote=None, key=None)
def test_page_info_tab_id(self, b, mock_send):
mock_send.return_value = {"title": "X"}
assert b.page.info(tab_id=42) == {"title": "X"}
mock_send.assert_called_once_with("tabs.status", {"tabId": 42}, profile=None, remote=None, key=None)
def test_storage_get(self, b, mock_send):
mock_send.return_value = "v"
assert b.storage.get("k") == "v"
+18 -2
View File
@@ -796,13 +796,29 @@ def test_windows_open_passes_url():
assert "https://example.com" in result.output
send_command.assert_called_once_with("windows.open", {"url": "https://example.com"}, profile=None, remote=None, key=None)
def test_page_info_command_with_tab():
with patch("browser_cli.send_command", return_value={"title": "Example", "url": "https://example.com"}) as send_command:
result = CliRunner().invoke(main, ["page", "info", "--tab", "42"])
assert result.exit_code == 0
assert "Example" in result.output
send_command.assert_called_once_with("tabs.status", {"tabId": 42}, profile=None, remote=None, key=None)
def test_extract_markdown_command():
with patch("browser_cli.send_command", return_value="# Title") as send_command:
result = CliRunner().invoke(main, ["extract", "markdown"])
assert result.exit_code == 0
assert result.output == "# Title\n"
send_command.assert_called_once_with("extract.markdown", {"selector": None}, profile=None, remote=None, key=None)
send_command.assert_called_once_with("extract.markdown", {"selector": None, "tabId": None}, profile=None, remote=None, key=None)
def test_extract_markdown_command_with_tab():
with patch("browser_cli.send_command", return_value="# Title") as send_command:
result = CliRunner().invoke(main, ["extract", "markdown", "--tab", "42"])
assert result.exit_code == 0
assert result.output == "# Title\n"
send_command.assert_called_once_with("extract.markdown", {"selector": None, "tabId": 42}, profile=None, remote=None, key=None)
def test_extract_markdown_command_with_selector():
with patch("browser_cli.send_command", return_value="## Post") as send_command:
@@ -810,7 +826,7 @@ def test_extract_markdown_command_with_selector():
assert result.exit_code == 0
assert result.output == "## Post\n"
send_command.assert_called_once_with("extract.markdown", {"selector": "article"}, profile=None, remote=None, key=None)
send_command.assert_called_once_with("extract.markdown", {"selector": "article", "tabId": None}, profile=None, remote=None, key=None)
def test_clean_markdown_output_removes_escaped_underscores_and_dashes():
assert _clean_markdown_output(r"hello\_world \- item") == "hello_world - item"
+26 -5
View File
@@ -62,14 +62,17 @@ class FakeClient:
def navigate_to(self, tab_id, url):
self.calls.append(("navigate", tab_id, url))
def page_info(self):
return {"title": "Example", "url": "https://example.com"}
def page_info(self, tab_id=None):
self.calls.append(("page_info", tab_id))
return {"title": "Example", "url": "https://example.com", "tab_id": tab_id}
def extract_text(self):
def extract_text(self, tab_id=None):
self.calls.append(("extract_text", tab_id))
return "Page text"
def extract_markdown(self, selector=None):
return f"# Page {selector or ''}".rstrip()
def extract_markdown(self, selector=None, tab_id=None):
self.calls.append(("extract_markdown", selector, tab_id))
return f"# Page {selector or ''} {tab_id or ''}".rstrip()
def dom_query(self, selector):
return [{"tag": "button", "selector": selector}]
@@ -112,6 +115,7 @@ async def test_each_tool_call_constructs_a_fresh_targeted_client(client, monkeyp
assert second.structured_content == {
"title": "Example",
"url": "https://example.com",
"tab_id": None,
}
assert len(FakeClient.instances) == 2
assert FakeClient.instances[0].target == ("work", "browser-host.example:443", "agent")
@@ -195,6 +199,23 @@ async def test_tab_tools_default_to_the_active_tab(client):
]
assert FakeClient.instances[1].calls == [("active",), ("close", 7)]
@pytest.mark.anyio
async def test_read_tools_accept_explicit_tab_id(client):
info = await client.call_tool("browser_page_info", {"tab_id": 42})
text = await client.call_tool("browser_extract_text", {"tab_id": 42})
markdown = await client.call_tool("browser_extract_markdown", {"selector": "main", "tab_id": 42})
assert info.structured_content == {
"id": 42,
"title": "Navigated",
"url": "https://example.com/next",
}
assert text.content[0].text == "Page text"
assert markdown.content[0].text == "# Page main 42"
assert FakeClient.instances[0].calls == [("status", 42)]
assert FakeClient.instances[1].calls == [("extract_text", 42)]
assert FakeClient.instances[2].calls == [("extract_markdown", "main", 42)]
@pytest.mark.anyio
async def test_screenshot_returns_image_content(client):
result = await client.call_tool("browser_screenshot", {"tab_id": 7, "format": "png"})