02cd9d519c
Lint & Test / test (push) Successful in 1m56s
- Rename Python package: wled_controller -> ledgrab - Rename env var prefix: WLED_ -> LEDGRAB_ (with auto-migration for old vars) - Rename localStorage key: wled_api_key -> ledgrab_api_key (with migration) - Rename HA integration domain: wled_screen_controller -> ledgrab - Update all imports, build scripts, Docker, installer, config, docs - Remove HA integration (moved to ledgrab-haos-integration repo) - Remove hacs.json (belongs in HA repo now) - Add startup warning for users with old WLED_ env vars - All tests pass (715/715), ruff clean, tsc clean, frontend builds
184 lines
6.5 KiB
Python
184 lines
6.5 KiB
Python
"""Tests for League of Legends Live Client Data API adapter."""
|
|
|
|
import threading
|
|
|
|
import pytest
|
|
|
|
from ledgrab.core.game_integration.adapters.lol_adapter import (
|
|
LoLAdapter,
|
|
LoLPoller,
|
|
)
|
|
|
|
|
|
def _make_lol_payload(
|
|
*,
|
|
current_health: float = 1000.0,
|
|
max_health: float = 1000.0,
|
|
resource_value: float = 500.0,
|
|
resource_max: float = 500.0,
|
|
level: int = 10,
|
|
summoner_name: str = "TestPlayer",
|
|
current_gold: float | None = None,
|
|
) -> dict:
|
|
"""Build a realistic LoL Live Client Data payload."""
|
|
payload: dict = {
|
|
"activePlayer": {
|
|
"summonerName": summoner_name,
|
|
"level": level,
|
|
"currentHealth": current_health,
|
|
"championStats": {
|
|
"currentHealth": current_health,
|
|
"maxHealth": max_health,
|
|
"resourceValue": resource_value,
|
|
"resourceMax": resource_max,
|
|
"attackDamage": 80.0,
|
|
"abilityPower": 0.0,
|
|
"armor": 60.0,
|
|
"magicResist": 40.0,
|
|
"moveSpeed": 345.0,
|
|
},
|
|
},
|
|
"allPlayers": [
|
|
{
|
|
"summonerName": summoner_name,
|
|
"championName": "Jinx",
|
|
"team": "ORDER",
|
|
"scores": {"kills": 5, "deaths": 2, "assists": 7},
|
|
},
|
|
],
|
|
"gameData": {
|
|
"gameMode": "CLASSIC",
|
|
"gameTime": 1200.5,
|
|
"mapName": "Map11",
|
|
"mapNumber": 11,
|
|
"mapTerrain": "Default",
|
|
},
|
|
}
|
|
|
|
if current_gold is not None:
|
|
payload["allPlayers"][0]["currentGold"] = current_gold
|
|
|
|
return payload
|
|
|
|
|
|
class TestLoLContinuousEvents:
|
|
def test_health_normalized(self) -> None:
|
|
payload = _make_lol_payload(current_health=500.0, max_health=1000.0)
|
|
events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, {})
|
|
hp = [e for e in events if e.event_type == "health"]
|
|
assert len(hp) == 1
|
|
assert hp[0].value == pytest.approx(0.5)
|
|
|
|
def test_mana_normalized(self) -> None:
|
|
payload = _make_lol_payload(resource_value=300.0, resource_max=600.0)
|
|
events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, {})
|
|
mp = [e for e in events if e.event_type == "mana"]
|
|
assert len(mp) == 1
|
|
assert mp[0].value == pytest.approx(0.5)
|
|
|
|
def test_level_normalized(self) -> None:
|
|
payload = _make_lol_payload(level=9)
|
|
events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, {})
|
|
lvl = [e for e in events if e.event_type == "speed"]
|
|
assert len(lvl) == 1
|
|
assert lvl[0].value == pytest.approx(9.0 / 18.0)
|
|
|
|
def test_level_max(self) -> None:
|
|
payload = _make_lol_payload(level=18)
|
|
events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, {})
|
|
lvl = [e for e in events if e.event_type == "speed"]
|
|
assert lvl[0].value == pytest.approx(1.0)
|
|
|
|
def test_gold(self) -> None:
|
|
payload = _make_lol_payload(current_gold=15000.0)
|
|
events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, {})
|
|
gold = [e for e in events if e.event_type == "gold"]
|
|
assert len(gold) == 1
|
|
assert gold[0].value == pytest.approx(15000.0 / 30000.0)
|
|
|
|
|
|
class TestLoLDeathRespawn:
|
|
def test_death_detected_when_health_drops_to_zero(self) -> None:
|
|
prev = {"alive": True}
|
|
payload = _make_lol_payload(current_health=0.0, max_health=1000.0)
|
|
events, state = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, prev)
|
|
deaths = [e for e in events if e.event_type == "death"]
|
|
assert len(deaths) == 1
|
|
assert state["alive"] is False
|
|
|
|
def test_no_death_when_already_dead(self) -> None:
|
|
prev = {"alive": False}
|
|
payload = _make_lol_payload(current_health=0.0, max_health=1000.0)
|
|
events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, prev)
|
|
deaths = [e for e in events if e.event_type == "death"]
|
|
assert len(deaths) == 0
|
|
|
|
def test_respawn_detected(self) -> None:
|
|
prev = {"alive": False}
|
|
payload = _make_lol_payload(current_health=800.0, max_health=1000.0)
|
|
events, state = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, prev)
|
|
respawns = [e for e in events if e.event_type == "objective_progress"]
|
|
assert len(respawns) == 1
|
|
assert state["alive"] is True
|
|
|
|
def test_no_respawn_when_already_alive(self) -> None:
|
|
prev = {"alive": True}
|
|
payload = _make_lol_payload(current_health=800.0, max_health=1000.0)
|
|
events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, prev)
|
|
respawns = [e for e in events if e.event_type == "objective_progress"]
|
|
assert len(respawns) == 0
|
|
|
|
|
|
class TestLoLAuth:
|
|
def test_always_accepts(self) -> None:
|
|
assert LoLAdapter.validate_auth({}, {}, {}) is True
|
|
assert LoLAdapter.validate_auth({"X-Custom": "val"}, {}, {"auth_token": "x"}) is True
|
|
|
|
|
|
class TestLoLMetadata:
|
|
def test_adapter_type(self) -> None:
|
|
assert LoLAdapter.ADAPTER_TYPE == "lol"
|
|
|
|
def test_supported_events(self) -> None:
|
|
assert "health" in LoLAdapter.SUPPORTED_EVENTS
|
|
assert "mana" in LoLAdapter.SUPPORTED_EVENTS
|
|
assert "death" in LoLAdapter.SUPPORTED_EVENTS
|
|
|
|
def test_config_schema(self) -> None:
|
|
schema = LoLAdapter.get_config_schema()
|
|
assert "poll_interval_ms" in schema["properties"]
|
|
|
|
def test_setup_instructions(self) -> None:
|
|
instructions = LoLAdapter.get_setup_instructions()
|
|
assert "League of Legends" in instructions
|
|
|
|
|
|
class TestLoLPoller:
|
|
def test_start_stop(self) -> None:
|
|
"""Test that the poller starts and stops cleanly."""
|
|
called = threading.Event()
|
|
|
|
def callback(data: dict) -> None:
|
|
called.set()
|
|
|
|
poller = LoLPoller({"poll_interval_ms": 100}, callback)
|
|
assert not poller.is_running
|
|
|
|
poller.start()
|
|
assert poller.is_running
|
|
|
|
poller.stop()
|
|
assert not poller.is_running
|
|
|
|
def test_double_start_no_crash(self) -> None:
|
|
"""Starting twice should not create duplicate threads."""
|
|
poller = LoLPoller({"poll_interval_ms": 1000}, lambda d: None)
|
|
poller.start()
|
|
poller.start() # should warn but not crash
|
|
poller.stop()
|
|
|
|
def test_stop_without_start(self) -> None:
|
|
"""Stopping without starting should not crash."""
|
|
poller = LoLPoller({"poll_interval_ms": 1000}, lambda d: None)
|
|
poller.stop() # no-op
|