Files
ledgrab/server/tests/test_preferences_card_modes_api.py
T
alexei.dolgolyov 75ca487be1 feat(ui): per-surface card presentation modes (C/M/D/R)
Adds a comfortable/compact/dense/row toggle to every card grid in the
app. Each surface (LED devices, targets, automations, scenes, sources,
streams, dashboard subsections, etc.) remembers its mode independently.

Persistence mirrors dashboard-layout: localStorage cache for first paint,
debounced PUT to /api/v1/preferences/card-modes (new endpoint) for
cross-browser sync. Surface registry is open — any non-empty key
accepted server-side; modes validated against {comfortable, compact,
dense, row}.

CSS is token-driven: grid min-width and gap come from --card-grid-min /
--card-grid-gap / --card-grid-min-narrow / --card-grid-gap-narrow /
--templates-grid-min / --templates-grid-gap defined on :root, overridden
per [data-card-mode]. Dense/row also hide .mod-leds, collapse secondary
button labels, and tighten .mod-metrics; row collapses the grid to one
full-width column. Coexists with the existing per-section [data-density]
on the dashboard tab — different attribute, additive concern.

Toggle UI auto-mounts into every CardSection header (18+ surfaces) plus
the six dashboard subsections via post-render mount; teardown tracking
keeps the listener Set bounded across re-renders.

i18n: card_mode.{tooltip,comfortable,compact,dense,row} in en/ru/zh.
Tests: 9 new cases in tests/test_preferences_card_modes_api.py covering
defaults, round-trip, validation, open-registry keys, row mode, delete.
2026-05-10 23:49:14 +03:00

120 lines
3.8 KiB
Python

"""Tests for /api/v1/preferences/card-modes endpoints."""
import pytest
from ledgrab.config import get_config
@pytest.fixture(scope="module")
def client():
"""TestClient with auth header read at fixture-build time.
Mirrors test_preferences_api.py — the auth key is resolved here so
any singleton mutation during pytest collection cannot leave us with
a stale Bearer header.
"""
from fastapi.testclient import TestClient
from ledgrab.main import app
api_key = next(iter(get_config().auth.api_keys.values()), "")
with TestClient(app, raise_server_exceptions=False) as c:
if api_key:
c.headers["Authorization"] = f"Bearer {api_key}"
yield c
def _prefs(surfaces: dict[str, str] | None = None) -> dict:
return {"version": 1, "surfaces": surfaces or {}}
def test_get_default_empty(client):
"""When nothing is saved, GET returns {}."""
client.delete("/api/v1/preferences/card-modes")
resp = client.get("/api/v1/preferences/card-modes")
assert resp.status_code == 200
assert resp.json() == {}
def test_put_then_get_round_trip(client):
"""PUT a prefs object, GET it back verbatim."""
body = _prefs({"led-devices": "dense", "led-targets": "comfortable"})
put = client.put("/api/v1/preferences/card-modes", json=body)
assert put.status_code == 200
assert put.json() == {"ok": True}
got = client.get("/api/v1/preferences/card-modes")
assert got.status_code == 200
assert got.json() == body
def test_put_rejects_missing_version(client):
"""Body without numeric version is rejected with 422."""
resp = client.put(
"/api/v1/preferences/card-modes",
json={"surfaces": {"led-devices": "dense"}},
)
assert resp.status_code == 422
def test_put_rejects_invalid_mode(client):
"""A mode value outside the allowed set is rejected with 422."""
resp = client.put(
"/api/v1/preferences/card-modes",
json=_prefs({"led-devices": "extreme"}),
)
assert resp.status_code == 422
def test_put_rejects_non_dict_surfaces(client):
"""`surfaces` must be an object, not an array or string."""
resp = client.put(
"/api/v1/preferences/card-modes",
json={"version": 1, "surfaces": ["led-devices", "dense"]},
)
assert resp.status_code == 422
def test_put_accepts_empty_surfaces(client):
"""An empty surfaces map is a valid (no-override) state."""
resp = client.put("/api/v1/preferences/card-modes", json=_prefs({}))
assert resp.status_code == 200
def test_put_accepts_unknown_surface_keys(client):
"""Surface keys are an open registry — any non-empty string is OK."""
body = _prefs(
{
"led-devices": "compact",
"automations": "dense",
"weather-sources": "comfortable",
"future-surface-v2": "compact",
}
)
resp = client.put("/api/v1/preferences/card-modes", json=body)
assert resp.status_code == 200
got = client.get("/api/v1/preferences/card-modes").json()
assert got["surfaces"]["future-surface-v2"] == "compact"
def test_put_accepts_row_mode(client):
"""`row` is a valid mode (added alongside the original three)."""
body = _prefs({"led-devices": "row"})
resp = client.put("/api/v1/preferences/card-modes", json=body)
assert resp.status_code == 200
got = client.get("/api/v1/preferences/card-modes").json()
assert got["surfaces"]["led-devices"] == "row"
def test_delete_clears(client):
"""DELETE wipes saved prefs so the next GET returns empty."""
client.put(
"/api/v1/preferences/card-modes",
json=_prefs({"led-devices": "dense"}),
)
deleted = client.delete("/api/v1/preferences/card-modes")
assert deleted.status_code == 200
after = client.get("/api/v1/preferences/card-modes")
assert after.status_code == 200
assert after.json() == {}