Widget system Phase 3: calendar widgets become size-aware
calendar_render.py's _build_* functions now take a real target box and pick font sizes/margins from three discrete size tiers (nearest pixel-area fit) instead of always laying out at full panel size and resizing after the fact -- a calendar widget placed smaller than the full panel gets an actually-legible layout instead of shrunk text. Month view falls back to agenda below the smallest tier, where 7 columns can no longer stay readable. The old photo-inlay split (inlay_region/_content_region/_paste_inlay) is deleted along with it -- arbitrary widget placement already subsumes what a fixed half-panel split did, and every call site has passed photo_inlay=None since the Phase 2 cutover. Also adds HTTP-level test coverage for GET .../preview/calendar, which had none before this -- it's what caught a stale photo_inlay kwarg left over from the _build signature change that would have TypeError'd on every request.
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
"""GET /api/frames/{id}/preview/calendar -- the Calendar tab's live
|
||||
render preview. No prior coverage existed for this endpoint; added
|
||||
after a Phase 3 refactor (calendar_render.py's size-tier rewrite, see
|
||||
the widget-system plan) left a stale photo_inlay=None kwarg here that
|
||||
would have TypeError'd on the very next request -- nothing in the
|
||||
existing suite actually called this endpoint to catch it."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
from app.models import CalendarWidgetConfig, Frame, FrameCalendar, Widget
|
||||
|
||||
from .conftest import csrf_headers
|
||||
|
||||
|
||||
def _configure_calendar_widget(db_session) -> Widget:
|
||||
client_frame = db_session.get(Frame, 1)
|
||||
widget = Widget(frame_id=client_frame.id, widget_type="calendar", x=0, y=0, w=8, h=5,
|
||||
sort_order=1, created_at=time.time())
|
||||
db_session.add(widget)
|
||||
db_session.flush()
|
||||
db_session.add(CalendarWidgetConfig(widget_id=widget.id, view="agenda"))
|
||||
db_session.commit()
|
||||
return widget
|
||||
|
||||
|
||||
def test_preview_calendar_requires_a_calendar_widget(client, db_session):
|
||||
client.post("/setup", data={"username": "alice", "password": "hunter22"})
|
||||
resp = client.get("/api/frames/1/preview/calendar")
|
||||
assert resp.status_code == 400
|
||||
assert "widget" in resp.json()["detail"].lower()
|
||||
|
||||
|
||||
def test_preview_calendar_requires_an_included_calendar(client, db_session):
|
||||
client.post("/setup", data={"username": "alice", "password": "hunter22"})
|
||||
_configure_calendar_widget(db_session)
|
||||
resp = client.get("/api/frames/1/preview/calendar")
|
||||
assert resp.status_code == 400
|
||||
assert "calendar" in resp.json()["detail"].lower()
|
||||
|
||||
|
||||
def test_preview_calendar_renders_a_png(client, db_session, monkeypatch):
|
||||
client.post("/setup", data={"username": "alice", "password": "hunter22"})
|
||||
_configure_calendar_widget(db_session)
|
||||
alice = db_session.get(Frame, 1).owner
|
||||
alice.calendar_ics_url = "http://example.invalid/alice.ics"
|
||||
db_session.add(FrameCalendar(frame_id=1, user_id=alice.id, calendar_key="ics",
|
||||
calendar_label="My calendar", included=True))
|
||||
db_session.commit()
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.routers.api_frames.get_or_refresh_calendar_events_for_widget",
|
||||
lambda db, frame, widget: ([], ""),
|
||||
)
|
||||
|
||||
resp = client.get("/api/frames/1/preview/calendar", headers=csrf_headers(client))
|
||||
assert resp.status_code == 200, resp.text
|
||||
assert resp.headers["content-type"] == "image/png"
|
||||
assert resp.content[:8] == b"\x89PNG\r\n\x1a\n"
|
||||
@@ -43,12 +43,11 @@ def test_render_produces_a_correctly_sized_image(db_session, monkeypatch):
|
||||
assert img.mode == "RGB"
|
||||
|
||||
|
||||
def test_render_resizes_when_target_differs_from_full_panel(db_session, monkeypatch):
|
||||
"""Phase-1 calendar widgets are still full-panel-layout internally
|
||||
(see app/widgets/calendar.py's own module docstring) -- resizing to
|
||||
fit whatever target box is asked for keeps render_panel's contract
|
||||
(exact target_w x target_h) satisfied even before real small-widget
|
||||
layout support lands."""
|
||||
def test_render_produces_a_correctly_sized_image_below_full_panel(db_session, monkeypatch):
|
||||
"""A calendar widget placed smaller than the full panel must still
|
||||
come back at exactly the target box -- render_panel's contract --
|
||||
now laid out directly at that size (see calendar_render._size_tier)
|
||||
rather than built full-size and resized down after the fact."""
|
||||
frame, widget = _make_widget(db_session, view="week")
|
||||
_stub_fetches(monkeypatch)
|
||||
|
||||
@@ -56,6 +55,37 @@ def test_render_resizes_when_target_differs_from_full_panel(db_session, monkeypa
|
||||
assert img.size == (250, 150)
|
||||
|
||||
|
||||
def test_render_at_minimum_grid_footprint_for_every_view(db_session, monkeypatch):
|
||||
"""grid.MIN_FOOTPRINT["calendar"] is (3, 2) cells -- on an 8x5 grid
|
||||
against a full 800x480 panel that's a 300x192 box, the smallest a
|
||||
calendar widget can actually be placed at. Every view (including
|
||||
month, which falls back to agenda below the "small" size tier -- see
|
||||
calendar_render._month_view_fits) must still render at exactly that
|
||||
size without error."""
|
||||
from app.calendar_render import CALENDAR_VIEWS
|
||||
|
||||
_stub_fetches(monkeypatch, events=[
|
||||
{"summary": "Standup", "start": "2026-08-01T09:00:00+00:00", "end": "2026-08-01T09:15:00+00:00",
|
||||
"all_day": False, "sources": [{"owner_display_name": "Alice", "color_index": None}]},
|
||||
])
|
||||
for view in CALENDAR_VIEWS:
|
||||
frame, widget = _make_widget(db_session, view=view)
|
||||
img = widgets.calendar.render(db_session, frame, widget, 300, 192)
|
||||
assert img.size == (300, 192), view
|
||||
|
||||
|
||||
def test_render_at_representative_footprints(db_session, monkeypatch):
|
||||
"""A handful of footprints spanning all three size tiers (see
|
||||
calendar_render._size_tier) -- small (below half-panel), medium
|
||||
(roughly half-panel, the old photo-inlay's proportions), and large
|
||||
(full panel) -- render without error at exactly the requested size."""
|
||||
_stub_fetches(monkeypatch)
|
||||
for target_w, target_h in [(300, 192), (400, 480), (800, 480)]:
|
||||
frame, widget = _make_widget(db_session, view="agenda")
|
||||
img = widgets.calendar.render(db_session, frame, widget, target_w, target_h)
|
||||
assert img.size == (target_w, target_h)
|
||||
|
||||
|
||||
def test_weather_only_fetched_when_enabled(db_session, monkeypatch):
|
||||
frame, widget = _make_widget(db_session, view="agenda", weather_enabled=False)
|
||||
calls = []
|
||||
@@ -125,3 +155,29 @@ def test_button_triggered_render_does_not_reset_browse_offset(db_session, monkey
|
||||
|
||||
widgets.calendar.render(db_session, frame, widget, 400, 300, is_normal_wake=False)
|
||||
assert db_session.get(CalendarWidgetConfig, widget.id).browse_offset == 1
|
||||
|
||||
|
||||
# --- calendar_render's size tiers ---
|
||||
|
||||
def test_size_tier_thresholds():
|
||||
from app.calendar_render import _size_tier
|
||||
|
||||
assert _size_tier(800, 480) == "large" # full panel
|
||||
assert _size_tier(400, 480) == "medium" # old photo-inlay's half-panel split
|
||||
assert _size_tier(300, 192) == "small" # grid.MIN_FOOTPRINT["calendar"]'s 3x2 cells
|
||||
|
||||
|
||||
def test_month_view_falls_back_to_agenda_layout_below_small_tier(db_session, monkeypatch):
|
||||
"""Month view needs real column width to stay legible -- at the
|
||||
minimum calendar footprint, render() should still succeed (producing
|
||||
an agenda-shaped render instead of an unreadable grid), not error or
|
||||
silently draw garbage."""
|
||||
from app.calendar_render import _month_view_fits
|
||||
|
||||
assert not _month_view_fits(300, 192)
|
||||
assert _month_view_fits(800, 480)
|
||||
|
||||
frame, widget = _make_widget(db_session, view="month")
|
||||
_stub_fetches(monkeypatch)
|
||||
img = widgets.calendar.render(db_session, frame, widget, 300, 192)
|
||||
assert img.size == (300, 192)
|
||||
|
||||
Reference in New Issue
Block a user