feat: add plugin dependency pipeline and HTML rendering service
- Implemented a new dependency pipeline for plugins to manage Python package dependencies, including conflict detection and automatic installation of missing dependencies. - Introduced an HTML rendering service that utilizes existing browsers to render HTML content as PNG images, with support for various configurations and error handling.
This commit is contained in:
194
pytests/test_html_render_service.py
Normal file
194
pytests/test_html_render_service.py
Normal file
@@ -0,0 +1,194 @@
|
||||
"""HTML 浏览器渲染服务测试。"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import pytest
|
||||
|
||||
from src.config.official_configs import PluginRuntimeRenderConfig
|
||||
from src.services import html_render_service as html_render_service_module
|
||||
from src.services.html_render_service import HTMLRenderService, ManagedBrowserRecord
|
||||
|
||||
|
||||
class _FakeChromium:
|
||||
"""用于模拟 Playwright Chromium 启动器的测试桩。"""
|
||||
|
||||
def __init__(self, effects: List[Any]) -> None:
|
||||
"""初始化 Chromium 启动测试桩。
|
||||
|
||||
Args:
|
||||
effects: 每次调用 ``launch`` 时依次返回或抛出的结果。
|
||||
"""
|
||||
|
||||
self._effects: List[Any] = list(effects)
|
||||
self.calls: List[Dict[str, Any]] = []
|
||||
|
||||
async def launch(self, **kwargs: Any) -> Any:
|
||||
"""模拟 Playwright Chromium 的启动过程。
|
||||
|
||||
Args:
|
||||
**kwargs: 浏览器启动参数。
|
||||
|
||||
Returns:
|
||||
Any: 预设的浏览器对象。
|
||||
|
||||
Raises:
|
||||
Exception: 当预设结果为异常对象时抛出。
|
||||
"""
|
||||
|
||||
self.calls.append(dict(kwargs))
|
||||
effect = self._effects.pop(0)
|
||||
if isinstance(effect, Exception):
|
||||
raise effect
|
||||
return effect
|
||||
|
||||
|
||||
class _FakePlaywright:
|
||||
"""用于模拟 Playwright 根对象的测试桩。"""
|
||||
|
||||
def __init__(self, chromium: _FakeChromium) -> None:
|
||||
"""初始化 Playwright 测试桩。
|
||||
|
||||
Args:
|
||||
chromium: Chromium 启动器测试桩。
|
||||
"""
|
||||
|
||||
self.chromium = chromium
|
||||
|
||||
|
||||
def _build_render_config(**kwargs: Any) -> PluginRuntimeRenderConfig:
|
||||
"""构造用于测试的浏览器渲染配置。
|
||||
|
||||
Args:
|
||||
**kwargs: 需要覆盖的配置字段。
|
||||
|
||||
Returns:
|
||||
PluginRuntimeRenderConfig: 测试使用的配置对象。
|
||||
"""
|
||||
|
||||
payload: Dict[str, Any] = {
|
||||
"auto_download_chromium": True,
|
||||
"browser_install_root": "data/test-playwright-browsers",
|
||||
}
|
||||
payload.update(kwargs)
|
||||
return PluginRuntimeRenderConfig(**payload)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_launch_browser_auto_downloads_chromium_when_missing(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
"""未检测到可用浏览器时,应自动下载 Chromium 并记录状态。"""
|
||||
|
||||
monkeypatch.setattr(html_render_service_module, "PROJECT_ROOT", tmp_path)
|
||||
service = HTMLRenderService()
|
||||
config = _build_render_config()
|
||||
fake_browser = object()
|
||||
fake_chromium = _FakeChromium(
|
||||
[
|
||||
RuntimeError("browserType.launch: Executable doesn't exist at /tmp/chromium"),
|
||||
fake_browser,
|
||||
]
|
||||
)
|
||||
install_calls: List[str] = []
|
||||
|
||||
monkeypatch.setattr(service, "_resolve_executable_path", lambda _config: "")
|
||||
|
||||
async def fake_install(_config: PluginRuntimeRenderConfig) -> None:
|
||||
"""模拟 Chromium 自动下载。
|
||||
|
||||
Args:
|
||||
_config: 当前浏览器渲染配置。
|
||||
"""
|
||||
|
||||
install_calls.append(_config.browser_install_root)
|
||||
browsers_path = service._get_managed_browsers_path(_config)
|
||||
(browsers_path / "chromium-1234").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
monkeypatch.setattr(service, "_install_chromium_browser", fake_install)
|
||||
|
||||
browser = await service._launch_browser(_FakePlaywright(fake_chromium), config)
|
||||
|
||||
assert browser is fake_browser
|
||||
assert install_calls == ["data/test-playwright-browsers"]
|
||||
assert len(fake_chromium.calls) == 2
|
||||
|
||||
browser_record = service._load_managed_browser_record()
|
||||
assert browser_record is not None
|
||||
assert browser_record.install_source == "auto_download"
|
||||
assert browser_record.browser_name == "chromium"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_launch_browser_reuses_existing_managed_browser(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
"""已存在 Playwright 托管浏览器时,不应重复下载。"""
|
||||
|
||||
monkeypatch.setattr(html_render_service_module, "PROJECT_ROOT", tmp_path)
|
||||
service = HTMLRenderService()
|
||||
config = _build_render_config()
|
||||
browsers_path = service._get_managed_browsers_path(config)
|
||||
(browsers_path / "chrome-headless-shell-1234").mkdir(parents=True, exist_ok=True)
|
||||
fake_browser = object()
|
||||
fake_chromium = _FakeChromium([fake_browser])
|
||||
|
||||
monkeypatch.setattr(service, "_resolve_executable_path", lambda _config: "")
|
||||
|
||||
async def fail_install(_config: PluginRuntimeRenderConfig) -> None:
|
||||
"""若被错误调用则立即失败。
|
||||
|
||||
Args:
|
||||
_config: 当前浏览器渲染配置。
|
||||
|
||||
Raises:
|
||||
AssertionError: 表示本测试不期望进入下载逻辑。
|
||||
"""
|
||||
|
||||
raise AssertionError("不应触发自动下载")
|
||||
|
||||
monkeypatch.setattr(service, "_install_chromium_browser", fail_install)
|
||||
|
||||
browser = await service._launch_browser(_FakePlaywright(fake_chromium), config)
|
||||
|
||||
assert browser is fake_browser
|
||||
assert len(fake_chromium.calls) == 1
|
||||
|
||||
browser_record = service._load_managed_browser_record()
|
||||
assert browser_record is not None
|
||||
assert browser_record.install_source == "existing_cache"
|
||||
assert browser_record.browsers_path == str(browsers_path)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_launch_browser_prefers_local_executable(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
"""探测到本机浏览器时,应优先使用可执行文件路径启动。"""
|
||||
|
||||
monkeypatch.setattr(html_render_service_module, "PROJECT_ROOT", tmp_path)
|
||||
service = HTMLRenderService()
|
||||
config = _build_render_config()
|
||||
fake_browser = object()
|
||||
fake_chromium = _FakeChromium([fake_browser])
|
||||
executable_path = "/usr/bin/google-chrome"
|
||||
|
||||
monkeypatch.setattr(service, "_resolve_executable_path", lambda _config: executable_path)
|
||||
|
||||
browser = await service._launch_browser(_FakePlaywright(fake_chromium), config)
|
||||
|
||||
assert browser is fake_browser
|
||||
assert len(fake_chromium.calls) == 1
|
||||
assert fake_chromium.calls[0]["executable_path"] == executable_path
|
||||
assert service._load_managed_browser_record() is None
|
||||
|
||||
|
||||
def test_managed_browser_record_roundtrip() -> None:
|
||||
"""托管浏览器记录应支持序列化与反序列化。"""
|
||||
|
||||
record = ManagedBrowserRecord(
|
||||
browser_name="chromium",
|
||||
browsers_path="/tmp/playwright-browsers",
|
||||
install_source="auto_download",
|
||||
playwright_version="1.58.0",
|
||||
recorded_at="2026-04-03T10:00:00+00:00",
|
||||
last_verified_at="2026-04-03T10:00:01+00:00",
|
||||
)
|
||||
|
||||
restored_record = ManagedBrowserRecord.from_dict(record.to_dict())
|
||||
|
||||
assert restored_record == record
|
||||
225
pytests/test_plugin_dependency_pipeline.py
Normal file
225
pytests/test_plugin_dependency_pipeline.py
Normal file
@@ -0,0 +1,225 @@
|
||||
"""插件依赖流水线测试。"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from src.plugin_runtime.dependency_pipeline import PluginDependencyPipeline
|
||||
|
||||
|
||||
def _build_manifest(
|
||||
plugin_id: str,
|
||||
*,
|
||||
dependencies: list[dict[str, str]] | None = None,
|
||||
) -> dict[str, object]:
|
||||
"""构造测试用的 Manifest v2 数据。
|
||||
|
||||
Args:
|
||||
plugin_id: 插件 ID。
|
||||
dependencies: 依赖声明列表。
|
||||
|
||||
Returns:
|
||||
dict[str, object]: 可直接写入 ``_manifest.json`` 的字典。
|
||||
"""
|
||||
|
||||
return {
|
||||
"manifest_version": 2,
|
||||
"version": "1.0.0",
|
||||
"name": plugin_id,
|
||||
"description": "测试插件",
|
||||
"author": {
|
||||
"name": "tester",
|
||||
"url": "https://example.com/tester",
|
||||
},
|
||||
"license": "MIT",
|
||||
"urls": {
|
||||
"repository": f"https://example.com/{plugin_id}",
|
||||
},
|
||||
"host_application": {
|
||||
"min_version": "1.0.0",
|
||||
"max_version": "1.0.0",
|
||||
},
|
||||
"sdk": {
|
||||
"min_version": "2.0.0",
|
||||
"max_version": "2.99.99",
|
||||
},
|
||||
"dependencies": dependencies or [],
|
||||
"capabilities": [],
|
||||
"i18n": {
|
||||
"default_locale": "zh-CN",
|
||||
"supported_locales": ["zh-CN"],
|
||||
},
|
||||
"id": plugin_id,
|
||||
}
|
||||
|
||||
|
||||
def _write_plugin(
|
||||
plugin_root: Path,
|
||||
plugin_name: str,
|
||||
plugin_id: str,
|
||||
*,
|
||||
dependencies: list[dict[str, str]] | None = None,
|
||||
) -> Path:
|
||||
"""在临时目录中写入一个测试插件。
|
||||
|
||||
Args:
|
||||
plugin_root: 插件根目录。
|
||||
plugin_name: 插件目录名。
|
||||
plugin_id: 插件 ID。
|
||||
dependencies: Python 依赖声明列表。
|
||||
|
||||
Returns:
|
||||
Path: 插件目录路径。
|
||||
"""
|
||||
|
||||
plugin_dir = plugin_root / plugin_name
|
||||
plugin_dir.mkdir(parents=True)
|
||||
(plugin_dir / "plugin.py").write_text("def create_plugin():\n return object()\n", encoding="utf-8")
|
||||
(plugin_dir / "_manifest.json").write_text(
|
||||
json.dumps(_build_manifest(plugin_id, dependencies=dependencies)),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return plugin_dir
|
||||
|
||||
|
||||
def test_build_plan_blocks_plugin_conflicting_with_host_requirement(tmp_path: Path) -> None:
|
||||
"""与主程序依赖冲突的插件应被阻止加载。"""
|
||||
|
||||
plugin_root = tmp_path / "plugins"
|
||||
_write_plugin(
|
||||
plugin_root,
|
||||
"conflict_plugin",
|
||||
"test.conflict-plugin",
|
||||
dependencies=[
|
||||
{
|
||||
"type": "python_package",
|
||||
"name": "numpy",
|
||||
"version_spec": "<1.0.0",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
pipeline = PluginDependencyPipeline(project_root=Path.cwd())
|
||||
plan = pipeline.build_plan([plugin_root])
|
||||
|
||||
assert "test.conflict-plugin" in plan.blocked_plugin_reasons
|
||||
assert "主程序" in plan.blocked_plugin_reasons["test.conflict-plugin"]
|
||||
assert plan.install_requirements == ()
|
||||
|
||||
|
||||
def test_build_plan_blocks_plugins_with_conflicting_python_dependencies(tmp_path: Path) -> None:
|
||||
"""插件之间出现 Python 包版本冲突时应同时阻止双方加载。"""
|
||||
|
||||
plugin_root = tmp_path / "plugins"
|
||||
_write_plugin(
|
||||
plugin_root,
|
||||
"plugin_a",
|
||||
"test.plugin-a",
|
||||
dependencies=[
|
||||
{
|
||||
"type": "python_package",
|
||||
"name": "demo-package",
|
||||
"version_spec": "<2.0.0",
|
||||
}
|
||||
],
|
||||
)
|
||||
_write_plugin(
|
||||
plugin_root,
|
||||
"plugin_b",
|
||||
"test.plugin-b",
|
||||
dependencies=[
|
||||
{
|
||||
"type": "python_package",
|
||||
"name": "demo-package",
|
||||
"version_spec": ">=3.0.0",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
pipeline = PluginDependencyPipeline(project_root=Path.cwd())
|
||||
plan = pipeline.build_plan([plugin_root])
|
||||
|
||||
assert "test.plugin-a" in plan.blocked_plugin_reasons
|
||||
assert "test.plugin-b" in plan.blocked_plugin_reasons
|
||||
assert "test.plugin-b" in plan.blocked_plugin_reasons["test.plugin-a"]
|
||||
assert "test.plugin-a" in plan.blocked_plugin_reasons["test.plugin-b"]
|
||||
|
||||
|
||||
def test_build_plan_collects_install_requirements_for_missing_packages(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""未安装但无冲突的依赖应进入自动安装计划。"""
|
||||
|
||||
plugin_root = tmp_path / "plugins"
|
||||
_write_plugin(
|
||||
plugin_root,
|
||||
"plugin_a",
|
||||
"test.plugin-a",
|
||||
dependencies=[
|
||||
{
|
||||
"type": "python_package",
|
||||
"name": "demo-package",
|
||||
"version_spec": ">=1.0.0,<2.0.0",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
pipeline = PluginDependencyPipeline(project_root=Path.cwd())
|
||||
monkeypatch.setattr(
|
||||
pipeline._manifest_validator,
|
||||
"get_installed_package_version",
|
||||
lambda package_name: None if package_name == "demo-package" else "1.0.0",
|
||||
)
|
||||
|
||||
plan = pipeline.build_plan([plugin_root])
|
||||
|
||||
assert plan.blocked_plugin_reasons == {}
|
||||
assert len(plan.install_requirements) == 1
|
||||
assert plan.install_requirements[0].package_name == "demo-package"
|
||||
assert plan.install_requirements[0].plugin_ids == ("test.plugin-a",)
|
||||
assert plan.install_requirements[0].requirement_text == "demo-package>=1.0.0,<2.0.0"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_blocks_plugins_when_auto_install_fails(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""自动安装失败时,相关插件应被阻止加载。"""
|
||||
|
||||
plugin_root = tmp_path / "plugins"
|
||||
_write_plugin(
|
||||
plugin_root,
|
||||
"plugin_a",
|
||||
"test.plugin-a",
|
||||
dependencies=[
|
||||
{
|
||||
"type": "python_package",
|
||||
"name": "demo-package",
|
||||
"version_spec": ">=1.0.0,<2.0.0",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
pipeline = PluginDependencyPipeline(project_root=Path.cwd())
|
||||
monkeypatch.setattr(
|
||||
pipeline._manifest_validator,
|
||||
"get_installed_package_version",
|
||||
lambda package_name: None if package_name == "demo-package" else "1.0.0",
|
||||
)
|
||||
|
||||
async def fake_install(_requirements) -> tuple[bool, str]:
|
||||
"""模拟依赖安装失败。"""
|
||||
|
||||
return False, "network error"
|
||||
|
||||
monkeypatch.setattr(pipeline, "_install_requirements", fake_install)
|
||||
|
||||
result = await pipeline.execute([plugin_root])
|
||||
|
||||
assert result.environment_changed is False
|
||||
assert "test.plugin-a" in result.blocked_plugin_reasons
|
||||
assert "自动安装 Python 依赖失败" in result.blocked_plugin_reasons["test.plugin-a"]
|
||||
@@ -3,6 +3,8 @@
|
||||
验证协议层、传输层、RPC 通信链路的正确性。
|
||||
"""
|
||||
|
||||
# pyright: reportArgumentType=false, reportAttributeAccessIssue=false, reportCallIssue=false, reportIndexIssue=false, reportMissingImports=false, reportOptionalMemberAccess=false
|
||||
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, Awaitable, Callable, Dict, List, Optional, Sequence
|
||||
@@ -2891,7 +2893,7 @@ class TestIntegration:
|
||||
assert instances[0].stopped is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_plugin_source_changes_only_reload_matching_supervisor(self, monkeypatch, tmp_path):
|
||||
async def test_handle_plugin_source_changes_restarts_supervisors_after_dependency_sync(self, monkeypatch, tmp_path):
|
||||
from src.config.file_watcher import FileChange
|
||||
from src.plugin_runtime import integration as integration_module
|
||||
import json
|
||||
@@ -2915,7 +2917,6 @@ class TestIntegration:
|
||||
def __init__(self, plugin_dirs, registered_plugins):
|
||||
self._plugin_dirs = plugin_dirs
|
||||
self._registered_plugins = registered_plugins
|
||||
self.reload_reasons = []
|
||||
self.config_updates = []
|
||||
|
||||
def get_loaded_plugin_ids(self):
|
||||
@@ -2924,9 +2925,6 @@ class TestIntegration:
|
||||
def get_loaded_plugin_versions(self):
|
||||
return {plugin_id: "1.0.0" for plugin_id in self._registered_plugins}
|
||||
|
||||
async def reload_plugins(self, plugin_ids=None, reason="manual", external_available_plugins=None):
|
||||
self.reload_reasons.append((plugin_ids, reason, external_available_plugins or {}))
|
||||
|
||||
async def notify_plugin_config_updated(self, plugin_id, config_data, config_version=""):
|
||||
self.config_updates.append((plugin_id, config_data, config_version))
|
||||
return True
|
||||
@@ -2935,27 +2933,37 @@ class TestIntegration:
|
||||
manager._started = True
|
||||
manager._builtin_supervisor = FakeSupervisor([builtin_root], {"test.alpha": object()})
|
||||
manager._third_party_supervisor = FakeSupervisor([thirdparty_root], {"test.beta": object()})
|
||||
dependency_sync_calls = []
|
||||
restart_calls = []
|
||||
|
||||
async def fake_sync(plugin_dirs: Sequence[Path]) -> Any:
|
||||
"""记录依赖同步调用。"""
|
||||
|
||||
dependency_sync_calls.append(list(plugin_dirs))
|
||||
return integration_module.DependencySyncState(
|
||||
blocked_changed_plugin_ids={"test.beta"},
|
||||
environment_changed=False,
|
||||
)
|
||||
|
||||
async def fake_restart(reason: str) -> bool:
|
||||
"""记录 Supervisor 重启调用。"""
|
||||
|
||||
restart_calls.append(reason)
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(manager, "_sync_plugin_dependencies", fake_sync)
|
||||
monkeypatch.setattr(manager, "_restart_supervisors", fake_restart)
|
||||
|
||||
changes = [
|
||||
FileChange(change_type=1, path=beta_dir / "plugin.py"),
|
||||
]
|
||||
|
||||
refresh_calls = []
|
||||
|
||||
def fake_refresh() -> None:
|
||||
refresh_calls.append(True)
|
||||
|
||||
manager._refresh_plugin_config_watch_subscriptions = fake_refresh
|
||||
|
||||
await manager._handle_plugin_source_changes(changes)
|
||||
|
||||
assert manager._builtin_supervisor.reload_reasons == []
|
||||
assert manager._third_party_supervisor.reload_reasons == [
|
||||
(["test.beta"], "file_watcher", {"test.alpha": "1.0.0"})
|
||||
]
|
||||
assert dependency_sync_calls == [[builtin_root, thirdparty_root]]
|
||||
assert restart_calls == ["file_watcher_blocklist_changed"]
|
||||
assert manager._builtin_supervisor.config_updates == []
|
||||
assert manager._third_party_supervisor.config_updates == []
|
||||
assert refresh_calls == [True]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reload_plugins_globally_warns_and_skips_cross_supervisor_dependents(self, monkeypatch):
|
||||
|
||||
96
pytests/test_plugin_runtime_render.py
Normal file
96
pytests/test_plugin_runtime_render.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""插件运行时浏览器渲染能力测试。"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
|
||||
from src.plugin_runtime.integration import PluginRuntimeManager
|
||||
from src.plugin_runtime.host.supervisor import PluginSupervisor
|
||||
from src.services.html_render_service import HtmlRenderRequest, HtmlRenderResult
|
||||
|
||||
|
||||
class _FakeRenderService:
|
||||
"""用于替代真实浏览器渲染服务的测试桩。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""初始化测试桩。"""
|
||||
|
||||
self.last_request: Optional[HtmlRenderRequest] = None
|
||||
|
||||
async def render_html_to_png(self, request: HtmlRenderRequest) -> HtmlRenderResult:
|
||||
"""记录请求并返回固定的渲染结果。
|
||||
|
||||
Args:
|
||||
request: 当前渲染请求。
|
||||
|
||||
Returns:
|
||||
HtmlRenderResult: 固定的测试渲染结果。
|
||||
"""
|
||||
|
||||
self.last_request = request
|
||||
return HtmlRenderResult(
|
||||
image_base64="ZmFrZS1pbWFnZQ==",
|
||||
mime_type="image/png",
|
||||
width=640,
|
||||
height=480,
|
||||
render_ms=12,
|
||||
)
|
||||
|
||||
|
||||
def test_render_capability_is_registered() -> None:
|
||||
"""Host 注册能力时应包含 render.html2png。"""
|
||||
|
||||
manager = PluginRuntimeManager()
|
||||
supervisor = PluginSupervisor(plugin_dirs=[])
|
||||
|
||||
manager._register_capability_impls(supervisor)
|
||||
|
||||
assert "render.html2png" in supervisor.capability_service.list_capabilities()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_render_capability_forwards_request(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""render.html2png 应将请求透传给浏览器渲染服务。"""
|
||||
|
||||
from src.plugin_runtime.capabilities import render as render_capability_module
|
||||
|
||||
fake_service = _FakeRenderService()
|
||||
monkeypatch.setattr(render_capability_module, "get_html_render_service", lambda: fake_service)
|
||||
|
||||
manager = PluginRuntimeManager()
|
||||
result = await manager._cap_render_html2png(
|
||||
"demo.plugin",
|
||||
"render.html2png",
|
||||
{
|
||||
"html": "<body><div id='card'>hello</div></body>",
|
||||
"selector": "#card",
|
||||
"viewport": {"width": 1024, "height": 768},
|
||||
"device_scale_factor": 1.5,
|
||||
"full_page": False,
|
||||
"omit_background": True,
|
||||
"wait_until": "networkidle",
|
||||
"wait_for_selector": "#card",
|
||||
"wait_for_timeout_ms": 150,
|
||||
"timeout_ms": 3000,
|
||||
"allow_network": True,
|
||||
},
|
||||
)
|
||||
|
||||
assert result == {
|
||||
"success": True,
|
||||
"result": {
|
||||
"image_base64": "ZmFrZS1pbWFnZQ==",
|
||||
"mime_type": "image/png",
|
||||
"width": 640,
|
||||
"height": 480,
|
||||
"render_ms": 12,
|
||||
},
|
||||
}
|
||||
assert fake_service.last_request is not None
|
||||
assert fake_service.last_request.selector == "#card"
|
||||
assert fake_service.last_request.viewport_width == 1024
|
||||
assert fake_service.last_request.viewport_height == 768
|
||||
assert fake_service.last_request.device_scale_factor == 1.5
|
||||
assert fake_service.last_request.omit_background is True
|
||||
assert fake_service.last_request.wait_until == "networkidle"
|
||||
assert fake_service.last_request.allow_network is True
|
||||
Reference in New Issue
Block a user