mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-30 10:52:15 +00:00
- Rewrite .github/workflows/desktop.yml: 2-job pipeline (validate + build-and-release) with rolling desktop-latest pre-release on push to main and stable desktop-v* releases - Add UpdateChecker component: checks for updates on startup + every 30 min, background download with progress bar, one-click relaunch - Configure Tauri updater: endpoints pointing to desktop-latest release, pubkey placeholder - Add tauri-plugin-process for relaunch support (Cargo.toml, lib.rs, package.json) - Add macOS Entitlements.plist for notarization (network + file access, no sandbox) - Add scripts/bump-desktop-version.sh for atomic version bumps across 3 config files - Add desktop/README.md with dev setup, auto-update architecture, signing docs - Update .gitignore for desktop/node_modules, dist, target - Configure macOS minimumSystemVersion, Windows timestampUrl - Include all Phase 14-21 work: agent hardening, RBAC, taint tracking, workflows, skills, knowledge graph, sessions, A2A, MCP templates, WASM sandbox, TUI dashboard, production tools, CLI expansion, API expansion, learning productionization, Tauri desktop app, and 10 new channels Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
908 lines
32 KiB
Python
908 lines
32 KiB
Python
"""Tests for browser automation tools."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
from unittest.mock import MagicMock, PropertyMock, patch
|
|
|
|
import pytest
|
|
|
|
from openjarvis.core.registry import ToolRegistry
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers — mock the _session before importing tools
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_mock_page():
|
|
"""Create a fully mocked Playwright page object."""
|
|
page = MagicMock()
|
|
page.title.return_value = "Test Page"
|
|
page.inner_text.return_value = "Hello World"
|
|
page.screenshot.return_value = b"\x89PNG\x00fake-screenshot-data"
|
|
|
|
response = MagicMock()
|
|
response.status = 200
|
|
page.goto.return_value = response
|
|
|
|
page.click.return_value = None
|
|
page.get_by_text.return_value = MagicMock()
|
|
page.fill.return_value = None
|
|
page.type.return_value = None
|
|
page.eval_on_selector_all.return_value = []
|
|
|
|
return page
|
|
|
|
|
|
def _make_mock_session(page=None):
|
|
"""Create a mocked _BrowserSession whose .page returns a mock page."""
|
|
if page is None:
|
|
page = _make_mock_page()
|
|
session = MagicMock()
|
|
type(session).page = PropertyMock(return_value=page)
|
|
return session
|
|
|
|
|
|
def _make_import_error_session():
|
|
"""Create a session whose .page raises ImportError."""
|
|
session = MagicMock()
|
|
type(session).page = PropertyMock(
|
|
side_effect=ImportError(
|
|
"playwright not installed. Install with: "
|
|
"pip install 'openjarvis[browser]'"
|
|
)
|
|
)
|
|
return session
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TestBrowserNavigateTool
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestBrowserNavigateTool:
|
|
def test_spec_name_and_category(self):
|
|
from openjarvis.tools.browser import BrowserNavigateTool
|
|
|
|
tool = BrowserNavigateTool()
|
|
assert tool.spec.name == "browser_navigate"
|
|
assert tool.spec.category == "browser"
|
|
|
|
def test_spec_requires_url_parameter(self):
|
|
from openjarvis.tools.browser import BrowserNavigateTool
|
|
|
|
tool = BrowserNavigateTool()
|
|
assert "url" in tool.spec.parameters["properties"]
|
|
assert "url" in tool.spec.parameters["required"]
|
|
|
|
def test_spec_has_wait_for_parameter(self):
|
|
from openjarvis.tools.browser import BrowserNavigateTool
|
|
|
|
tool = BrowserNavigateTool()
|
|
assert "wait_for" in tool.spec.parameters["properties"]
|
|
|
|
def test_spec_required_capabilities(self):
|
|
from openjarvis.tools.browser import BrowserNavigateTool
|
|
|
|
tool = BrowserNavigateTool()
|
|
assert "network:fetch" in tool.spec.required_capabilities
|
|
|
|
def test_tool_id(self):
|
|
from openjarvis.tools.browser import BrowserNavigateTool
|
|
|
|
tool = BrowserNavigateTool()
|
|
assert tool.tool_id == "browser_navigate"
|
|
|
|
def test_execute_no_url(self):
|
|
from openjarvis.tools.browser import BrowserNavigateTool
|
|
|
|
tool = BrowserNavigateTool()
|
|
result = tool.execute(url="")
|
|
assert result.success is False
|
|
assert "No URL" in result.content
|
|
|
|
def test_execute_no_url_param(self):
|
|
from openjarvis.tools.browser import BrowserNavigateTool
|
|
|
|
tool = BrowserNavigateTool()
|
|
result = tool.execute()
|
|
assert result.success is False
|
|
assert "No URL" in result.content
|
|
|
|
def test_execute_playwright_not_installed(self):
|
|
from openjarvis.tools.browser import BrowserNavigateTool
|
|
|
|
session = _make_import_error_session()
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserNavigateTool()
|
|
result = tool.execute(url="https://example.com")
|
|
assert result.success is False
|
|
assert "playwright not installed" in result.content
|
|
|
|
def test_execute_ssrf_blocked(self):
|
|
from openjarvis.tools.browser import BrowserNavigateTool
|
|
|
|
mock_ssrf_module = MagicMock()
|
|
mock_ssrf_module.check_ssrf.return_value = (
|
|
"Blocked host: 169.254.169.254 (cloud metadata endpoint)"
|
|
)
|
|
with patch.dict(
|
|
"sys.modules",
|
|
{"openjarvis.security.ssrf": mock_ssrf_module},
|
|
):
|
|
tool = BrowserNavigateTool()
|
|
result = tool.execute(url="http://169.254.169.254/latest/meta-data/")
|
|
|
|
assert result.success is False
|
|
assert "SSRF blocked" in result.content
|
|
|
|
def test_execute_ssrf_module_missing(self):
|
|
"""When ssrf module is not available, skip check and proceed."""
|
|
from openjarvis.tools.browser import BrowserNavigateTool
|
|
|
|
page = _make_mock_page()
|
|
session = _make_mock_session(page)
|
|
|
|
# Make the ssrf import fail inside execute
|
|
import builtins
|
|
original_import = builtins.__import__
|
|
|
|
def _mock_import(name, *args, **kwargs):
|
|
if name == "openjarvis.security.ssrf":
|
|
raise ImportError("No module named 'openjarvis.security.ssrf'")
|
|
return original_import(name, *args, **kwargs)
|
|
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
with patch.object(builtins, "__import__", side_effect=_mock_import):
|
|
tool = BrowserNavigateTool()
|
|
result = tool.execute(url="https://example.com")
|
|
# Should succeed since SSRF check is skipped
|
|
assert result.success is True
|
|
|
|
def test_execute_success(self):
|
|
from openjarvis.tools.browser import BrowserNavigateTool
|
|
|
|
page = _make_mock_page()
|
|
page.title.return_value = "Example Domain"
|
|
page.inner_text.return_value = "Example page content"
|
|
session = _make_mock_session(page)
|
|
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserNavigateTool()
|
|
result = tool.execute(url="https://example.com")
|
|
|
|
assert result.success is True
|
|
assert "Example Domain" in result.content
|
|
assert "Example page content" in result.content
|
|
assert result.metadata["url"] == "https://example.com"
|
|
assert result.metadata["title"] == "Example Domain"
|
|
assert result.metadata["status"] == 200
|
|
|
|
def test_execute_with_wait_for(self):
|
|
from openjarvis.tools.browser import BrowserNavigateTool
|
|
|
|
page = _make_mock_page()
|
|
session = _make_mock_session(page)
|
|
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserNavigateTool()
|
|
tool.execute(url="https://example.com", wait_for="networkidle")
|
|
|
|
page.goto.assert_called_once_with(
|
|
"https://example.com", wait_until="networkidle"
|
|
)
|
|
|
|
def test_execute_invalid_wait_for_defaults_to_load(self):
|
|
from openjarvis.tools.browser import BrowserNavigateTool
|
|
|
|
page = _make_mock_page()
|
|
session = _make_mock_session(page)
|
|
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserNavigateTool()
|
|
tool.execute(url="https://example.com", wait_for="invalid")
|
|
|
|
page.goto.assert_called_once_with(
|
|
"https://example.com", wait_until="load"
|
|
)
|
|
|
|
def test_execute_content_truncation(self):
|
|
from openjarvis.tools.browser import BrowserNavigateTool
|
|
|
|
page = _make_mock_page()
|
|
page.inner_text.return_value = "x" * 6000
|
|
session = _make_mock_session(page)
|
|
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserNavigateTool()
|
|
result = tool.execute(url="https://example.com")
|
|
|
|
assert result.success is True
|
|
assert "[Content truncated]" in result.content
|
|
|
|
def test_execute_navigation_error(self):
|
|
from openjarvis.tools.browser import BrowserNavigateTool
|
|
|
|
page = _make_mock_page()
|
|
page.goto.side_effect = Exception("net::ERR_NAME_NOT_RESOLVED")
|
|
session = _make_mock_session(page)
|
|
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserNavigateTool()
|
|
result = tool.execute(url="https://nonexistent.example")
|
|
|
|
assert result.success is False
|
|
assert "Navigation error" in result.content
|
|
|
|
def test_to_openai_function(self):
|
|
from openjarvis.tools.browser import BrowserNavigateTool
|
|
|
|
tool = BrowserNavigateTool()
|
|
fn = tool.to_openai_function()
|
|
assert fn["type"] == "function"
|
|
assert fn["function"]["name"] == "browser_navigate"
|
|
assert "url" in fn["function"]["parameters"]["properties"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TestBrowserClickTool
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestBrowserClickTool:
|
|
def test_spec_name_and_category(self):
|
|
from openjarvis.tools.browser import BrowserClickTool
|
|
|
|
tool = BrowserClickTool()
|
|
assert tool.spec.name == "browser_click"
|
|
assert tool.spec.category == "browser"
|
|
|
|
def test_spec_requires_selector(self):
|
|
from openjarvis.tools.browser import BrowserClickTool
|
|
|
|
tool = BrowserClickTool()
|
|
assert "selector" in tool.spec.parameters["properties"]
|
|
assert "selector" in tool.spec.parameters["required"]
|
|
|
|
def test_spec_has_by_text_parameter(self):
|
|
from openjarvis.tools.browser import BrowserClickTool
|
|
|
|
tool = BrowserClickTool()
|
|
assert "by_text" in tool.spec.parameters["properties"]
|
|
|
|
def test_tool_id(self):
|
|
from openjarvis.tools.browser import BrowserClickTool
|
|
|
|
tool = BrowserClickTool()
|
|
assert tool.tool_id == "browser_click"
|
|
|
|
def test_execute_no_selector(self):
|
|
from openjarvis.tools.browser import BrowserClickTool
|
|
|
|
tool = BrowserClickTool()
|
|
result = tool.execute(selector="")
|
|
assert result.success is False
|
|
assert "No selector" in result.content
|
|
|
|
def test_execute_no_selector_param(self):
|
|
from openjarvis.tools.browser import BrowserClickTool
|
|
|
|
tool = BrowserClickTool()
|
|
result = tool.execute()
|
|
assert result.success is False
|
|
assert "No selector" in result.content
|
|
|
|
def test_execute_playwright_not_installed(self):
|
|
from openjarvis.tools.browser import BrowserClickTool
|
|
|
|
session = _make_import_error_session()
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserClickTool()
|
|
result = tool.execute(selector="#btn")
|
|
assert result.success is False
|
|
assert "playwright not installed" in result.content
|
|
|
|
def test_execute_click_by_css(self):
|
|
from openjarvis.tools.browser import BrowserClickTool
|
|
|
|
page = _make_mock_page()
|
|
session = _make_mock_session(page)
|
|
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserClickTool()
|
|
result = tool.execute(selector="#submit-btn")
|
|
|
|
assert result.success is True
|
|
assert "Clicked element" in result.content
|
|
page.click.assert_called_once_with("#submit-btn")
|
|
assert result.metadata["selector"] == "#submit-btn"
|
|
assert result.metadata["by_text"] is False
|
|
|
|
def test_execute_click_by_text(self):
|
|
from openjarvis.tools.browser import BrowserClickTool
|
|
|
|
page = _make_mock_page()
|
|
session = _make_mock_session(page)
|
|
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserClickTool()
|
|
result = tool.execute(selector="Sign In", by_text=True)
|
|
|
|
assert result.success is True
|
|
page.get_by_text.assert_called_once_with("Sign In")
|
|
page.get_by_text.return_value.click.assert_called_once()
|
|
assert result.metadata["by_text"] is True
|
|
|
|
def test_execute_click_error(self):
|
|
from openjarvis.tools.browser import BrowserClickTool
|
|
|
|
page = _make_mock_page()
|
|
page.click.side_effect = Exception("Element not found")
|
|
session = _make_mock_session(page)
|
|
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserClickTool()
|
|
result = tool.execute(selector="#nonexistent")
|
|
|
|
assert result.success is False
|
|
assert "Click error" in result.content
|
|
|
|
def test_to_openai_function(self):
|
|
from openjarvis.tools.browser import BrowserClickTool
|
|
|
|
tool = BrowserClickTool()
|
|
fn = tool.to_openai_function()
|
|
assert fn["type"] == "function"
|
|
assert fn["function"]["name"] == "browser_click"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TestBrowserTypeTool
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestBrowserTypeTool:
|
|
def test_spec_name_and_category(self):
|
|
from openjarvis.tools.browser import BrowserTypeTool
|
|
|
|
tool = BrowserTypeTool()
|
|
assert tool.spec.name == "browser_type"
|
|
assert tool.spec.category == "browser"
|
|
|
|
def test_spec_requires_selector_and_text(self):
|
|
from openjarvis.tools.browser import BrowserTypeTool
|
|
|
|
tool = BrowserTypeTool()
|
|
assert "selector" in tool.spec.parameters["properties"]
|
|
assert "text" in tool.spec.parameters["properties"]
|
|
assert "selector" in tool.spec.parameters["required"]
|
|
assert "text" in tool.spec.parameters["required"]
|
|
|
|
def test_spec_has_clear_parameter(self):
|
|
from openjarvis.tools.browser import BrowserTypeTool
|
|
|
|
tool = BrowserTypeTool()
|
|
assert "clear" in tool.spec.parameters["properties"]
|
|
|
|
def test_tool_id(self):
|
|
from openjarvis.tools.browser import BrowserTypeTool
|
|
|
|
tool = BrowserTypeTool()
|
|
assert tool.tool_id == "browser_type"
|
|
|
|
def test_execute_no_selector(self):
|
|
from openjarvis.tools.browser import BrowserTypeTool
|
|
|
|
tool = BrowserTypeTool()
|
|
result = tool.execute(selector="", text="hello")
|
|
assert result.success is False
|
|
assert "No selector" in result.content
|
|
|
|
def test_execute_no_text(self):
|
|
from openjarvis.tools.browser import BrowserTypeTool
|
|
|
|
tool = BrowserTypeTool()
|
|
result = tool.execute(selector="#input", text="")
|
|
assert result.success is False
|
|
assert "No text" in result.content
|
|
|
|
def test_execute_no_params(self):
|
|
from openjarvis.tools.browser import BrowserTypeTool
|
|
|
|
tool = BrowserTypeTool()
|
|
result = tool.execute()
|
|
assert result.success is False
|
|
|
|
def test_execute_playwright_not_installed(self):
|
|
from openjarvis.tools.browser import BrowserTypeTool
|
|
|
|
session = _make_import_error_session()
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserTypeTool()
|
|
result = tool.execute(selector="#input", text="hello")
|
|
assert result.success is False
|
|
assert "playwright not installed" in result.content
|
|
|
|
def test_execute_fill_clear_true(self):
|
|
from openjarvis.tools.browser import BrowserTypeTool
|
|
|
|
page = _make_mock_page()
|
|
session = _make_mock_session(page)
|
|
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserTypeTool()
|
|
result = tool.execute(selector="#search", text="query", clear=True)
|
|
|
|
assert result.success is True
|
|
page.fill.assert_called_once_with("#search", "query")
|
|
page.type.assert_not_called()
|
|
assert result.metadata["selector"] == "#search"
|
|
|
|
def test_execute_fill_default_clear(self):
|
|
"""Default clear=True should use page.fill()."""
|
|
from openjarvis.tools.browser import BrowserTypeTool
|
|
|
|
page = _make_mock_page()
|
|
session = _make_mock_session(page)
|
|
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserTypeTool()
|
|
result = tool.execute(selector="#search", text="query")
|
|
|
|
assert result.success is True
|
|
page.fill.assert_called_once_with("#search", "query")
|
|
|
|
def test_execute_type_clear_false(self):
|
|
from openjarvis.tools.browser import BrowserTypeTool
|
|
|
|
page = _make_mock_page()
|
|
session = _make_mock_session(page)
|
|
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserTypeTool()
|
|
result = tool.execute(selector="#search", text="query", clear=False)
|
|
|
|
assert result.success is True
|
|
page.type.assert_called_once_with("#search", "query")
|
|
page.fill.assert_not_called()
|
|
|
|
def test_execute_type_error(self):
|
|
from openjarvis.tools.browser import BrowserTypeTool
|
|
|
|
page = _make_mock_page()
|
|
page.fill.side_effect = Exception("Element not editable")
|
|
session = _make_mock_session(page)
|
|
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserTypeTool()
|
|
result = tool.execute(selector="#readonly", text="hello")
|
|
|
|
assert result.success is False
|
|
assert "Type error" in result.content
|
|
|
|
def test_to_openai_function(self):
|
|
from openjarvis.tools.browser import BrowserTypeTool
|
|
|
|
tool = BrowserTypeTool()
|
|
fn = tool.to_openai_function()
|
|
assert fn["type"] == "function"
|
|
assert fn["function"]["name"] == "browser_type"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TestBrowserScreenshotTool
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestBrowserScreenshotTool:
|
|
def test_spec_name_and_category(self):
|
|
from openjarvis.tools.browser import BrowserScreenshotTool
|
|
|
|
tool = BrowserScreenshotTool()
|
|
assert tool.spec.name == "browser_screenshot"
|
|
assert tool.spec.category == "browser"
|
|
|
|
def test_spec_has_path_and_full_page(self):
|
|
from openjarvis.tools.browser import BrowserScreenshotTool
|
|
|
|
tool = BrowserScreenshotTool()
|
|
props = tool.spec.parameters["properties"]
|
|
assert "path" in props
|
|
assert "full_page" in props
|
|
|
|
def test_spec_no_required_params(self):
|
|
from openjarvis.tools.browser import BrowserScreenshotTool
|
|
|
|
tool = BrowserScreenshotTool()
|
|
assert "required" not in tool.spec.parameters
|
|
|
|
def test_tool_id(self):
|
|
from openjarvis.tools.browser import BrowserScreenshotTool
|
|
|
|
tool = BrowserScreenshotTool()
|
|
assert tool.tool_id == "browser_screenshot"
|
|
|
|
def test_execute_playwright_not_installed(self):
|
|
from openjarvis.tools.browser import BrowserScreenshotTool
|
|
|
|
session = _make_import_error_session()
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserScreenshotTool()
|
|
result = tool.execute()
|
|
assert result.success is False
|
|
assert "playwright not installed" in result.content
|
|
|
|
def test_execute_screenshot_basic(self):
|
|
from openjarvis.tools.browser import BrowserScreenshotTool
|
|
|
|
fake_png = b"\x89PNG\x00screenshot-data"
|
|
page = _make_mock_page()
|
|
page.screenshot.return_value = fake_png
|
|
session = _make_mock_session(page)
|
|
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserScreenshotTool()
|
|
result = tool.execute()
|
|
|
|
assert result.success is True
|
|
assert "Screenshot taken" in result.content
|
|
assert "full page" not in result.content
|
|
expected_b64 = base64.b64encode(fake_png).decode("utf-8")
|
|
assert result.metadata["screenshot_base64"] == expected_b64
|
|
page.screenshot.assert_called_once_with(full_page=False)
|
|
|
|
def test_execute_screenshot_full_page(self):
|
|
from openjarvis.tools.browser import BrowserScreenshotTool
|
|
|
|
page = _make_mock_page()
|
|
page.screenshot.return_value = b"png-data"
|
|
session = _make_mock_session(page)
|
|
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserScreenshotTool()
|
|
result = tool.execute(full_page=True)
|
|
|
|
assert result.success is True
|
|
assert "full page" in result.content
|
|
page.screenshot.assert_called_once_with(full_page=True)
|
|
|
|
def test_execute_screenshot_save_to_file(self, tmp_path):
|
|
from openjarvis.tools.browser import BrowserScreenshotTool
|
|
|
|
fake_png = b"\x89PNGscreenshot"
|
|
page = _make_mock_page()
|
|
page.screenshot.return_value = fake_png
|
|
session = _make_mock_session(page)
|
|
|
|
save_path = str(tmp_path / "test_screenshot.png")
|
|
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserScreenshotTool()
|
|
result = tool.execute(path=save_path)
|
|
|
|
assert result.success is True
|
|
assert save_path in result.content
|
|
# Verify file was written
|
|
with open(save_path, "rb") as f:
|
|
assert f.read() == fake_png
|
|
|
|
def test_execute_screenshot_error(self):
|
|
from openjarvis.tools.browser import BrowserScreenshotTool
|
|
|
|
page = _make_mock_page()
|
|
page.screenshot.side_effect = Exception("Browser crashed")
|
|
session = _make_mock_session(page)
|
|
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserScreenshotTool()
|
|
result = tool.execute()
|
|
|
|
assert result.success is False
|
|
assert "Screenshot error" in result.content
|
|
|
|
def test_to_openai_function(self):
|
|
from openjarvis.tools.browser import BrowserScreenshotTool
|
|
|
|
tool = BrowserScreenshotTool()
|
|
fn = tool.to_openai_function()
|
|
assert fn["type"] == "function"
|
|
assert fn["function"]["name"] == "browser_screenshot"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TestBrowserExtractTool
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestBrowserExtractTool:
|
|
def test_spec_name_and_category(self):
|
|
from openjarvis.tools.browser import BrowserExtractTool
|
|
|
|
tool = BrowserExtractTool()
|
|
assert tool.spec.name == "browser_extract"
|
|
assert tool.spec.category == "browser"
|
|
|
|
def test_spec_has_selector_and_extract_type(self):
|
|
from openjarvis.tools.browser import BrowserExtractTool
|
|
|
|
tool = BrowserExtractTool()
|
|
props = tool.spec.parameters["properties"]
|
|
assert "selector" in props
|
|
assert "extract_type" in props
|
|
|
|
def test_tool_id(self):
|
|
from openjarvis.tools.browser import BrowserExtractTool
|
|
|
|
tool = BrowserExtractTool()
|
|
assert tool.tool_id == "browser_extract"
|
|
|
|
def test_execute_playwright_not_installed(self):
|
|
from openjarvis.tools.browser import BrowserExtractTool
|
|
|
|
session = _make_import_error_session()
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserExtractTool()
|
|
result = tool.execute()
|
|
assert result.success is False
|
|
assert "playwright not installed" in result.content
|
|
|
|
def test_execute_invalid_extract_type(self):
|
|
from openjarvis.tools.browser import BrowserExtractTool
|
|
|
|
tool = BrowserExtractTool()
|
|
result = tool.execute(extract_type="images")
|
|
assert result.success is False
|
|
assert "Invalid extract_type" in result.content
|
|
|
|
def test_execute_extract_text(self):
|
|
from openjarvis.tools.browser import BrowserExtractTool
|
|
|
|
page = _make_mock_page()
|
|
page.inner_text.return_value = "Page text content here"
|
|
session = _make_mock_session(page)
|
|
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserExtractTool()
|
|
result = tool.execute(extract_type="text")
|
|
|
|
assert result.success is True
|
|
assert result.content == "Page text content here"
|
|
page.inner_text.assert_called_with("body")
|
|
assert result.metadata["extract_type"] == "text"
|
|
|
|
def test_execute_extract_text_custom_selector(self):
|
|
from openjarvis.tools.browser import BrowserExtractTool
|
|
|
|
page = _make_mock_page()
|
|
page.inner_text.return_value = "Article content"
|
|
session = _make_mock_session(page)
|
|
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserExtractTool()
|
|
result = tool.execute(selector="#article", extract_type="text")
|
|
|
|
assert result.success is True
|
|
page.inner_text.assert_called_with("#article")
|
|
assert result.metadata["selector"] == "#article"
|
|
|
|
def test_execute_extract_text_default(self):
|
|
"""Default extract_type should be 'text'."""
|
|
from openjarvis.tools.browser import BrowserExtractTool
|
|
|
|
page = _make_mock_page()
|
|
page.inner_text.return_value = "Default text"
|
|
session = _make_mock_session(page)
|
|
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserExtractTool()
|
|
result = tool.execute()
|
|
|
|
assert result.success is True
|
|
assert result.content == "Default text"
|
|
|
|
def test_execute_extract_text_truncation(self):
|
|
from openjarvis.tools.browser import BrowserExtractTool
|
|
|
|
page = _make_mock_page()
|
|
page.inner_text.return_value = "a" * 12000
|
|
session = _make_mock_session(page)
|
|
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserExtractTool()
|
|
result = tool.execute(extract_type="text")
|
|
|
|
assert result.success is True
|
|
assert "[Content truncated]" in result.content
|
|
# Content should be truncated at 10000 + truncation notice
|
|
assert len(result.content) < 11000
|
|
|
|
def test_execute_extract_links(self):
|
|
from openjarvis.tools.browser import BrowserExtractTool
|
|
|
|
page = _make_mock_page()
|
|
page.eval_on_selector_all.return_value = [
|
|
{"href": "https://example.com/page1", "text": "Page 1"},
|
|
{"href": "https://example.com/page2", "text": "Page 2"},
|
|
]
|
|
session = _make_mock_session(page)
|
|
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserExtractTool()
|
|
result = tool.execute(extract_type="links")
|
|
|
|
assert result.success is True
|
|
assert "[Page 1](https://example.com/page1)" in result.content
|
|
assert "[Page 2](https://example.com/page2)" in result.content
|
|
assert result.metadata["num_links"] == 2
|
|
|
|
def test_execute_extract_links_empty(self):
|
|
from openjarvis.tools.browser import BrowserExtractTool
|
|
|
|
page = _make_mock_page()
|
|
page.eval_on_selector_all.return_value = []
|
|
session = _make_mock_session(page)
|
|
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserExtractTool()
|
|
result = tool.execute(extract_type="links")
|
|
|
|
assert result.success is True
|
|
assert result.content == "No links found."
|
|
assert result.metadata["num_links"] == 0
|
|
|
|
def test_execute_extract_tables(self):
|
|
from openjarvis.tools.browser import BrowserExtractTool
|
|
|
|
page = _make_mock_page()
|
|
page.eval_on_selector_all.return_value = [
|
|
"Name\tAge\nAlice\t30",
|
|
"City\tCountry\nNYC\tUSA",
|
|
]
|
|
session = _make_mock_session(page)
|
|
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserExtractTool()
|
|
result = tool.execute(extract_type="tables")
|
|
|
|
assert result.success is True
|
|
assert "Alice" in result.content
|
|
assert "NYC" in result.content
|
|
assert result.metadata["num_tables"] == 2
|
|
|
|
def test_execute_extract_tables_empty(self):
|
|
from openjarvis.tools.browser import BrowserExtractTool
|
|
|
|
page = _make_mock_page()
|
|
page.eval_on_selector_all.return_value = []
|
|
session = _make_mock_session(page)
|
|
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserExtractTool()
|
|
result = tool.execute(extract_type="tables")
|
|
|
|
assert result.success is True
|
|
assert result.content == "No tables found."
|
|
|
|
def test_execute_extract_error(self):
|
|
from openjarvis.tools.browser import BrowserExtractTool
|
|
|
|
page = _make_mock_page()
|
|
page.inner_text.side_effect = Exception("Selector not found")
|
|
session = _make_mock_session(page)
|
|
|
|
with patch("openjarvis.tools.browser._session", session):
|
|
tool = BrowserExtractTool()
|
|
result = tool.execute(selector="#missing", extract_type="text")
|
|
|
|
assert result.success is False
|
|
assert "Extract error" in result.content
|
|
|
|
def test_to_openai_function(self):
|
|
from openjarvis.tools.browser import BrowserExtractTool
|
|
|
|
tool = BrowserExtractTool()
|
|
fn = tool.to_openai_function()
|
|
assert fn["type"] == "function"
|
|
assert fn["function"]["name"] == "browser_extract"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TestBrowserSession
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestBrowserSession:
|
|
def test_session_close_resets_state(self):
|
|
from openjarvis.tools.browser import _BrowserSession
|
|
|
|
session = _BrowserSession()
|
|
session._playwright = MagicMock()
|
|
session._browser = MagicMock()
|
|
session._page = MagicMock()
|
|
|
|
session.close()
|
|
|
|
assert session._playwright is None
|
|
assert session._browser is None
|
|
assert session._page is None
|
|
|
|
def test_session_close_noop_when_not_initialized(self):
|
|
from openjarvis.tools.browser import _BrowserSession
|
|
|
|
session = _BrowserSession()
|
|
# Should not raise
|
|
session.close()
|
|
assert session._playwright is None
|
|
|
|
def test_session_ensure_browser_import_error(self):
|
|
from openjarvis.tools.browser import _BrowserSession
|
|
|
|
session = _BrowserSession()
|
|
|
|
import builtins
|
|
original_import = builtins.__import__
|
|
|
|
def _mock_import(name, *args, **kwargs):
|
|
if "playwright" in name:
|
|
raise ImportError("No module named 'playwright'")
|
|
return original_import(name, *args, **kwargs)
|
|
|
|
with patch.object(builtins, "__import__", side_effect=_mock_import):
|
|
with pytest.raises(ImportError, match="playwright not installed"):
|
|
session._ensure_browser()
|
|
|
|
def test_session_page_reuses_existing(self):
|
|
from openjarvis.tools.browser import _BrowserSession
|
|
|
|
session = _BrowserSession()
|
|
mock_page = MagicMock()
|
|
session._page = mock_page
|
|
|
|
# _ensure_browser should not re-create if page exists
|
|
session._ensure_browser()
|
|
assert session._page is mock_page
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TestRegistryIntegration
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestRegistryIntegration:
|
|
def test_all_tools_registered(self):
|
|
# Registration happens at import time via @ToolRegistry.register.
|
|
# Other test modules may clear the registry, so re-register if needed.
|
|
from openjarvis.tools.browser import (
|
|
BrowserClickTool,
|
|
BrowserExtractTool,
|
|
BrowserNavigateTool,
|
|
BrowserScreenshotTool,
|
|
BrowserTypeTool,
|
|
)
|
|
|
|
tools = {
|
|
"browser_navigate": BrowserNavigateTool,
|
|
"browser_click": BrowserClickTool,
|
|
"browser_type": BrowserTypeTool,
|
|
"browser_screenshot": BrowserScreenshotTool,
|
|
"browser_extract": BrowserExtractTool,
|
|
}
|
|
for key, cls in tools.items():
|
|
if not ToolRegistry.contains(key):
|
|
ToolRegistry.register_value(key, cls)
|
|
|
|
assert ToolRegistry.contains("browser_navigate")
|
|
assert ToolRegistry.contains("browser_click")
|
|
assert ToolRegistry.contains("browser_type")
|
|
assert ToolRegistry.contains("browser_screenshot")
|
|
assert ToolRegistry.contains("browser_extract")
|
|
|
|
def test_module_exports(self):
|
|
from openjarvis.tools.browser import __all__
|
|
|
|
assert "BrowserNavigateTool" in __all__
|
|
assert "BrowserClickTool" in __all__
|
|
assert "BrowserTypeTool" in __all__
|
|
assert "BrowserScreenshotTool" in __all__
|
|
assert "BrowserExtractTool" in __all__
|