Files
espresso_frame/server/tests/test_widget_font_scale.py
T
tfaour 2868087467
Build and push server image / test (push) Successful in 42s
Build and push server image / build-and-push (push) Successful in 3m33s
Build and push server image / deploy (push) Failing after 1m24s
Add a per-widget text-size picker for calendar/tasks legibility
Calendar and tasks pack the most body text at the smallest default
sizes, so those two gear-icon dialogs get a "Text size" card (Normal/
Large/X-Large) alongside the existing Border card -- a new Widget-level
font_scale column with its own POST .../font-scale endpoint, same
Widget-property-not-config-field shape as border_style. Threaded through
every classic (calendar_render.py) and modern (html_render.py/
calendar_html_render.py) size calc via one shared panel_style.
scaled_size() so row heights/max_rows already derived from font size
re-fit around the bigger text automatically.
2026-08-02 03:23:27 +00:00

185 lines
8.0 KiB
Python

"""routers/api_widgets.py's POST .../font-scale endpoint (models.Widget.
font_scale) -- a Widget-level property, not a per-type config field, same
reasoning/shape as test_widget_border.py's border coverage. The second
half confirms the actual render threading (widgets/calendar.py and
widgets/tasks.py pass widget.font_scale into both the classic and modern
builders), the same "spy on the resolve call" approach test_widgets_tasks.
py/test_widgets_calendar.py already use for frame.theme threading."""
from __future__ import annotations
import time
from app.models import CalendarWidgetConfig, Frame, TaskWidgetConfig, Widget
from .conftest import csrf_headers, link_user, login, make_user
def _widget_id(db_session, widget_type="photos") -> int:
return db_session.query(Widget).filter_by(frame_id=1, widget_type=widget_type).one().id
def test_new_widget_defaults_to_normal_font_scale(db_session):
widget = db_session.query(Widget).filter_by(frame_id=1).one()
assert widget.font_scale == 1.0
def test_set_font_scale_persists(client, db_session):
client.post("/setup", data={"username": "alice", "password": "hunter22"})
widget_id = _widget_id(db_session)
resp = client.post(f"/api/frames/1/widgets/{widget_id}/font-scale",
json={"font_scale": 1.25}, headers=csrf_headers(client))
assert resp.status_code == 200, resp.text
assert resp.json()["font_scale"] == 1.25
widget = db_session.get(Widget, widget_id)
assert widget.font_scale == 1.25
def test_set_font_scale_rejects_a_value_outside_the_fixed_choices(client, db_session):
client.post("/setup", data={"username": "alice", "password": "hunter22"})
widget_id = _widget_id(db_session)
resp = client.post(f"/api/frames/1/widgets/{widget_id}/font-scale",
json={"font_scale": 3.0}, headers=csrf_headers(client))
assert resp.status_code == 400
assert "font_scale" in resp.json()["detail"]
assert db_session.get(Widget, widget_id).font_scale == 1.0
def test_set_font_scale_404s_for_unknown_widget(client, db_session):
client.post("/setup", data={"username": "alice", "password": "hunter22"})
resp = client.post("/api/frames/1/widgets/999999/font-scale",
json={"font_scale": 1.25}, headers=csrf_headers(client))
assert resp.status_code == 404
def test_set_font_scale_unrelated_user_404s(client, db_session):
client.post("/setup", data={"username": "alice", "password": "hunter22"})
make_user(db_session, "mallory")
widget_id = _widget_id(db_session)
client.cookies.clear()
login(client, "mallory")
resp = client.post(f"/api/frames/1/widgets/{widget_id}/font-scale",
json={"font_scale": 1.25}, headers=csrf_headers(client))
assert resp.status_code == 404
assert db_session.get(Widget, widget_id).font_scale == 1.0
def test_set_font_scale_linked_but_not_controlling_user_409s(client, db_session):
client.post("/setup", data={"username": "alice", "password": "hunter22"})
bob = make_user(db_session, "bob")
frame = db_session.get(Frame, 1)
link_user(db_session, bob, frame)
widget_id = _widget_id(db_session)
client.cookies.clear()
login(client, "bob")
resp = client.post(f"/api/frames/1/widgets/{widget_id}/font-scale",
json={"font_scale": 1.25}, headers=csrf_headers(client))
assert resp.status_code == 409
assert resp.json()["detail"]["error"] == "not_controller"
# --- actually threads through to the renderers -----------------------------
def test_calendar_classic_render_threads_font_scale_through(db_session, monkeypatch):
"""Confirms widgets/calendar.py's classic branch passes widget.
font_scale into calendar_render._build, by spying on panel_style.
scaled_size (every classic builder's one shared scale point -- see
its own docstring)."""
from app import panel_style, widgets
frame = db_session.get(Frame, 1)
widget = Widget(frame_id=frame.id, widget_type="calendar", x=0, y=0, w=8, h=5,
sort_order=0, created_at=time.time(), font_scale=1.5)
db_session.add(widget)
db_session.flush()
db_session.add(CalendarWidgetConfig(widget_id=widget.id, view="agenda"))
db_session.commit()
monkeypatch.setattr(widgets.calendar, "get_or_refresh_calendar_events_for_widget",
lambda db, frame, widget: ([], ""))
monkeypatch.setattr(widgets.calendar, "get_or_refresh_weather_for_widget",
lambda db, frame, widget: [])
seen_scales = []
real_scaled_size = panel_style.scaled_size
monkeypatch.setattr(panel_style, "scaled_size", lambda v, s: (seen_scales.append(s), real_scaled_size(v, s))[1])
widgets.calendar.render(db_session, frame, widget, 300, 200)
assert seen_scales and all(s == 1.5 for s in seen_scales)
def test_calendar_modern_render_threads_font_scale_through(db_session, monkeypatch):
from PIL import Image
from app import html_render, widgets
frame = db_session.get(Frame, 1)
widget = Widget(frame_id=frame.id, widget_type="calendar", x=0, y=0, w=8, h=5,
sort_order=0, created_at=time.time(), font_scale=1.25)
db_session.add(widget)
db_session.flush()
db_session.add(CalendarWidgetConfig(widget_id=widget.id, view="agenda", render_style="modern"))
db_session.commit()
monkeypatch.setattr(widgets.calendar, "get_or_refresh_calendar_events_for_widget",
lambda db, frame, widget: ([], ""))
monkeypatch.setattr(widgets.calendar, "get_or_refresh_weather_for_widget",
lambda db, frame, widget: [])
monkeypatch.setattr(html_render, "render_html_to_image",
lambda html, target_w, target_h: Image.new("RGB", (target_w, target_h), (255, 255, 255)))
from app import panel_style
seen_scales = []
real_scaled_size = panel_style.scaled_size
monkeypatch.setattr(panel_style, "scaled_size", lambda v, s: (seen_scales.append(s), real_scaled_size(v, s))[1])
widgets.calendar.render(db_session, frame, widget, 300, 200)
assert seen_scales and all(s == 1.25 for s in seen_scales)
def test_tasks_classic_render_threads_font_scale_through(db_session, monkeypatch):
from app import panel_style, widgets
frame = db_session.get(Frame, 1)
widget = Widget(frame_id=frame.id, widget_type="tasks", x=0, y=0, w=2, h=2,
sort_order=0, created_at=time.time(), font_scale=1.5)
db_session.add(widget)
db_session.flush()
db_session.add(TaskWidgetConfig(widget_id=widget.id))
db_session.commit()
monkeypatch.setattr(widgets.tasks, "get_or_refresh_tasks_for_widget", lambda db, frame, widget: [])
seen_scales = []
real_scaled_size = panel_style.scaled_size
monkeypatch.setattr(panel_style, "scaled_size", lambda v, s: (seen_scales.append(s), real_scaled_size(v, s))[1])
widgets.tasks.render(db_session, frame, widget, 300, 200)
assert seen_scales and all(s == 1.5 for s in seen_scales)
def test_tasks_modern_render_threads_font_scale_through(db_session, monkeypatch):
from PIL import Image
from app import html_render, panel_style, widgets
frame = db_session.get(Frame, 1)
widget = Widget(frame_id=frame.id, widget_type="tasks", x=0, y=0, w=2, h=2,
sort_order=0, created_at=time.time(), font_scale=1.25)
db_session.add(widget)
db_session.flush()
db_session.add(TaskWidgetConfig(widget_id=widget.id, render_style="modern"))
db_session.commit()
monkeypatch.setattr(widgets.tasks, "get_or_refresh_tasks_for_widget", lambda db, frame, widget: [])
monkeypatch.setattr(html_render, "render_html_to_image",
lambda html, target_w, target_h: Image.new("RGB", (target_w, target_h), (255, 255, 255)))
seen_scales = []
real_scaled_size = panel_style.scaled_size
monkeypatch.setattr(panel_style, "scaled_size", lambda v, s: (seen_scales.append(s), real_scaled_size(v, s))[1])
widgets.tasks.render(db_session, frame, widget, 300, 200)
assert seen_scales and all(s == 1.25 for s in seen_scales)