feat(read): target any tab for page info and extraction
- 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:
+14
-3
@@ -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
@@ -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
@@ -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"})
|
||||
|
||||
Reference in New Issue
Block a user