"""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)