Files
espresso_frame/server/tests/test_widget_config_and_queue_endpoints.py
T
Thomas Faour 4a2b1f3795
Build and push server image / test (push) Successful in 25s
Build and push server image / build-and-push (push) Successful in 1m59s
Build and push server image / deploy (push) Successful in 53s
Let a tasks widget have a custom on-panel name
Adds TaskWidgetConfig.name (migration 19, plain column add) shown at
the top of the widget on the actual panel instead of the hardcoded
"Tasks" header -- e.g. "Chores" or "Mom's list". The only widget type
with its own on-panel title at all, since it's the only one where
"which list is this" isn't already obvious from a calendar/photo/
whiteboard's own content.

Threaded through calendar_render's _draw_tasks/_build_tasks/
render_tasks/render_tasks_preview_png as a `title` param (default
"Tasks", truncated to fit -- a long custom name shouldn't be able to
overflow the widget's box), the config-save endpoint (tasks_name,
truncated server-side to a sane header length rather than rejected),
and a new "Settings" section in the tasks dialog.

Verified live in the browser: the name actually renders at the top of
the real composited panel (not just the dialog preview, which stays
gated on having a configured source), and persists correctly on both
desktop and mobile. Full suite (196 tests) passes.
2026-07-25 04:05:36 +00:00

203 lines
7.5 KiB
Python

"""api_widgets.py's config-save (form-urlencoded, dispatched by
widget_type) and the photo-queue endpoints it shares the file with --
the moved-and-consolidated counterparts of the old frame-level
api_config_save/api_queue/etc. No prior HTTP-level coverage existed for
either the old or new shape of these endpoints; added after manual
browser testing caught api_widget_config_save expecting a JSON body
while the (unmodified, copied-over) dialog JS posts form-urlencoded data
-- a real bug an HTTP-level test would have caught immediately."""
from __future__ import annotations
from app.models import CalendarWidgetConfig, Frame, PhotoWidgetConfig, TaskWidgetConfig, Widget
from .conftest import csrf_headers
_ASSETS = [{"id": "asset-1"}, {"id": "asset-2"}, {"id": "asset-3"}]
def _photo_widget(db_session) -> Widget:
return db_session.query(Widget).filter_by(frame_id=1, widget_type="photos").one()
def _add_calendar_widget(db_session) -> Widget:
import time
widget = Widget(frame_id=1, widget_type="calendar", x=0, y=0, w=3, h=2,
sort_order=1, created_at=time.time())
db_session.add(widget)
db_session.flush()
db_session.add(CalendarWidgetConfig(widget_id=widget.id))
db_session.commit()
return widget
def _add_tasks_widget(db_session) -> Widget:
import time
widget = Widget(frame_id=1, widget_type="tasks", x=0, y=0, w=2, h=2,
sort_order=1, created_at=time.time())
db_session.add(widget)
db_session.flush()
db_session.add(TaskWidgetConfig(widget_id=widget.id))
db_session.commit()
return widget
def _mock_immich(monkeypatch):
monkeypatch.setattr("app.routers.api_widgets.immich_client_for", lambda frame: object())
monkeypatch.setattr("app.routers.api_widgets.list_assets", lambda client, album_id: _ASSETS)
# --- api_widget_config_save ---
def test_config_save_updates_a_photos_widget(client, db_session):
client.post("/setup", data={"username": "alice", "password": "hunter22"})
widget = _photo_widget(db_session)
resp = client.post(
f"/api/frames/1/widgets/{widget.id}/config",
data={"album_id": "album-42", "order": "shuffle", "display_mode": "stretch_fill",
"queue_target_len": "15"},
headers=csrf_headers(client),
)
assert resp.status_code == 200, resp.text
cfg = db_session.get(PhotoWidgetConfig, widget.id)
assert cfg.album_id == "album-42"
assert cfg.order == "shuffle"
assert cfg.display_mode == "stretch_fill"
assert cfg.queue_target_len == 15
def test_config_save_updates_a_calendar_widget(client, db_session):
client.post("/setup", data={"username": "alice", "password": "hunter22"})
widget = _add_calendar_widget(db_session)
resp = client.post(
f"/api/frames/1/widgets/{widget.id}/config",
data={"calendar_view": "week", "calendar_week_start": "1", "calendar_week_days": "5",
"calendar_week_layout": "vertical", "calendar_weather_enabled": "true",
"calendar_weather_units": "celsius"},
headers=csrf_headers(client),
)
assert resp.status_code == 200, resp.text
cfg = db_session.get(CalendarWidgetConfig, widget.id)
assert cfg.view == "week"
assert cfg.week_start == 1
assert cfg.week_days == 5
assert cfg.week_layout == "vertical"
assert cfg.weather_enabled is True
assert cfg.weather_units == "celsius"
def test_config_save_updates_a_tasks_widget(client, db_session):
client.post("/setup", data={"username": "alice", "password": "hunter22"})
widget = _add_tasks_widget(db_session)
resp = client.post(
f"/api/frames/1/widgets/{widget.id}/config",
data={"tasks_name": "Chores", "tasks_show_completed": "true"},
headers=csrf_headers(client),
)
assert resp.status_code == 200, resp.text
cfg = db_session.get(TaskWidgetConfig, widget.id)
assert cfg.name == "Chores"
assert cfg.show_completed is True
def test_config_save_truncates_an_overlong_tasks_name(client, db_session):
client.post("/setup", data={"username": "alice", "password": "hunter22"})
widget = _add_tasks_widget(db_session)
resp = client.post(
f"/api/frames/1/widgets/{widget.id}/config",
data={"tasks_name": "A" * 200},
headers=csrf_headers(client),
)
assert resp.status_code == 200, resp.text
cfg = db_session.get(TaskWidgetConfig, widget.id)
assert len(cfg.name) == 40
def test_config_save_only_partially_updates_provided_fields(client, db_session):
"""Fields not present in the POST are left untouched -- the whole
point of the partial-update convention (each dialog's own form only
ever posts its own fields)."""
client.post("/setup", data={"username": "alice", "password": "hunter22"})
widget = _photo_widget(db_session)
cfg = db_session.get(PhotoWidgetConfig, widget.id)
cfg.order = "shuffle"
db_session.commit()
resp = client.post(
f"/api/frames/1/widgets/{widget.id}/config",
data={"queue_target_len": "10"},
headers=csrf_headers(client),
)
assert resp.status_code == 200, resp.text
cfg = db_session.get(PhotoWidgetConfig, widget.id)
assert cfg.queue_target_len == 10
assert cfg.order == "shuffle" # untouched
def test_config_save_calendar_fields_are_a_no_op_on_a_photos_widget(client, db_session):
"""Posting calendar-shaped fields at a photos widget's config
endpoint doesn't error -- it just doesn't apply, since the dispatch
is purely by widget.widget_type."""
client.post("/setup", data={"username": "alice", "password": "hunter22"})
widget = _photo_widget(db_session)
resp = client.post(
f"/api/frames/1/widgets/{widget.id}/config",
data={"calendar_view": "week"},
headers=csrf_headers(client),
)
assert resp.status_code == 200, resp.text
def test_config_save_404s_for_a_widget_id_that_does_not_exist(client, db_session):
client.post("/setup", data={"username": "alice", "password": "hunter22"})
resp = client.post("/api/frames/1/widgets/999999/config", data={"order": "shuffle"},
headers=csrf_headers(client))
assert resp.status_code == 404
# --- photo queue (moved from the old frame-level /api/frames/{id}/queue) ---
def test_queue_requires_a_configured_album(client, db_session):
client.post("/setup", data={"username": "alice", "password": "hunter22"})
widget = _photo_widget(db_session)
resp = client.get(f"/api/frames/1/widgets/{widget.id}/queue")
assert resp.status_code == 400
def test_queue_returns_current_and_upcoming(client, db_session, monkeypatch):
client.post("/setup", data={"username": "alice", "password": "hunter22"})
frame = db_session.get(Frame, 1)
frame.immich_url = "http://immich.example.com"
frame.immich_api_key = "key"
widget = _photo_widget(db_session)
cfg = db_session.get(PhotoWidgetConfig, widget.id)
cfg.album_id = "album-1"
db_session.commit()
_mock_immich(monkeypatch)
resp = client.get(f"/api/frames/1/widgets/{widget.id}/queue")
assert resp.status_code == 200, resp.text
data = resp.json()
assert data["current"]["id"] == "asset-1"
assert [u["id"] for u in data["upcoming"]] == ["asset-2", "asset-3"]
assert data["control"]["you"] is True
def test_queue_400s_when_widget_is_not_photos(client, db_session):
client.post("/setup", data={"username": "alice", "password": "hunter22"})
widget = _add_calendar_widget(db_session)
resp = client.get(f"/api/frames/1/widgets/{widget.id}/queue")
assert resp.status_code == 400