71803418e5
Shelly device management app with mDNS/subnet discovery, inventory, configuration, and mass operations for Gen1/Gen2+ devices. Includes .gitignore excluding runtime data (device DB, user config), AI conversation history, build artifacts, and common Python/OS patterns.
94 lines
3.7 KiB
Python
94 lines
3.7 KiB
Python
"""Streamlit AppTest — no live server (uses embedded script runner)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
from streamlit.testing.v1 import AppTest
|
|
|
|
from shelly_manager.core.config import AppConfig
|
|
from shelly_manager.core.models import ShellyDevice
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
DASHBOARD_APP = ROOT / "src" / "shelly_manager" / "ui" / "Dashboard.py"
|
|
MASS_CONFIG_PAGE = ROOT / "src" / "shelly_manager" / "ui" / "pages" / "3_Mass_Config.py"
|
|
DEVICE_PAGE = ROOT / "src" / "shelly_manager" / "ui" / "pages" / "2_Device.py"
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_manager() -> MagicMock:
|
|
mgr = MagicMock()
|
|
mgr.storage.list_devices = AsyncMock(return_value=[])
|
|
mgr.storage.get_device = AsyncMock(return_value=None)
|
|
mgr.storage.get_kv = AsyncMock(return_value=None)
|
|
mgr.storage.set_kv = AsyncMock(return_value=None)
|
|
mgr.storage.list_snapshots = AsyncMock(return_value=[])
|
|
mgr.storage.recent_snapshot_labels_for_devices = AsyncMock(return_value={})
|
|
mgr.discover_all = AsyncMock(return_value=[])
|
|
mgr.refresh_all = AsyncMock(return_value=[])
|
|
mgr.refresh_device = AsyncMock(return_value=None)
|
|
mgr.mass_apply_operation = AsyncMock(return_value=[])
|
|
mgr.apply_mass_tags = AsyncMock(return_value=0)
|
|
mgr.apply_device_config = AsyncMock(return_value=[("sys", "ok", False)])
|
|
mgr.reboot_device = AsyncMock(return_value=None)
|
|
mgr.reboot_devices_by_ids = AsyncMock(return_value=[])
|
|
mgr.check_firmware_update = AsyncMock(return_value={})
|
|
mgr.check_firmware_updates_for_ids = AsyncMock(return_value=[])
|
|
mgr.apply_firmware_update = AsyncMock(return_value=None)
|
|
mgr.mass_apply_section_patch = AsyncMock(return_value=[])
|
|
return mgr
|
|
|
|
|
|
def test_dashboard_renders_title_and_metrics(mock_manager: MagicMock) -> None:
|
|
with patch("shelly_manager.ui.Dashboard.get_manager", return_value=mock_manager):
|
|
at = AppTest.from_file(str(DASHBOARD_APP), default_timeout=15)
|
|
at.run(timeout=15)
|
|
titles = [n.value for n in at.title]
|
|
assert any("Shelly Manager" in (t or "") for t in titles)
|
|
assert len(at.metric) >= 1
|
|
labels = [m.label for m in at.metric]
|
|
assert "Matching (after filters)" in labels
|
|
md_blob = "\n".join((m.value or "") for m in at.markdown)
|
|
assert "Total devices" in md_blob
|
|
|
|
|
|
def test_mass_config_page_renders(mock_manager: MagicMock) -> None:
|
|
with patch("shelly_manager.ui.state.get_manager", return_value=mock_manager):
|
|
at = AppTest.from_file(str(MASS_CONFIG_PAGE), default_timeout=20)
|
|
at.run(timeout=20)
|
|
assert not at.exception
|
|
titles = [n.value for n in at.title]
|
|
assert any("Mass configuration" in (t or "") for t in titles)
|
|
|
|
|
|
def test_device_detail_page_renders(mock_manager: MagicMock) -> None:
|
|
d = ShellyDevice(
|
|
id="AABBCCDDEEFF",
|
|
name="Kitchen",
|
|
mac="AA:BB:CC:DD:EE:FF",
|
|
ip="192.168.1.10",
|
|
generation=2,
|
|
model="Plus1",
|
|
firmware="1.0",
|
|
online=True,
|
|
last_seen=datetime.now(UTC),
|
|
capabilities=[],
|
|
status={},
|
|
settings={"sys": {"device": {"name": "Test"}}},
|
|
tags=[],
|
|
)
|
|
mock_manager.storage.list_devices = AsyncMock(return_value=[d])
|
|
mock_manager.storage.get_device = AsyncMock(return_value=d)
|
|
with (
|
|
patch("shelly_manager.ui.state.get_manager", return_value=mock_manager),
|
|
patch("shelly_manager.ui.state.get_config", return_value=AppConfig()),
|
|
):
|
|
at = AppTest.from_file(str(DEVICE_PAGE), default_timeout=20)
|
|
at.run(timeout=20)
|
|
assert not at.exception
|
|
titles = [n.value for n in at.title]
|
|
assert any("Device" in (t or "") for t in titles)
|