Files
espresso_frame/server/tests/test_widget_border.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

151 lines
6.9 KiB
Python

"""routers/api_widgets.py's POST .../border endpoint (models.Widget.
border_style/border_thickness/border_color_index) -- a Widget-level
property, not a per-type config field, so this is exercised independently
of any specific widget_type. End-to-end rendering (does a saved border
actually show up in the composited panel) is covered separately in
test_device_widget_dispatch.py-style tests below via the frame preview
endpoint, the same real render_panel path /frame/image uses."""
from __future__ import annotations
import io
from PIL import Image
from app.models import Frame, 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_no_border(db_session):
widget = db_session.query(Widget).filter_by(frame_id=1).one()
assert widget.border_style == "none"
assert widget.border_thickness == 3
assert widget.border_color_index == 0
def test_set_border_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}/border",
json={"border_style": "dashed", "border_thickness": 5, "border_color_index": 3},
headers=csrf_headers(client))
assert resp.status_code == 200, resp.text
assert resp.json() == {
"id": widget_id, "widget_type": "photos", "x": 0, "y": 0, "w": 8, "h": 5, "sort_order": 0,
"border_style": "dashed", "border_thickness": 5, "border_color_index": 3, "font_scale": 1.0,
"locked": False,
}
widget = db_session.get(Widget, widget_id)
assert (widget.border_style, widget.border_thickness, widget.border_color_index) == ("dashed", 5, 3)
def test_set_border_thickness_clamps_to_range(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}/border",
json={"border_style": "solid", "border_thickness": 999, "border_color_index": 0},
headers=csrf_headers(client))
assert resp.status_code == 200, resp.text
assert resp.json()["border_thickness"] == 8 # image_pipeline.MAX_BORDER_THICKNESS
resp = client.post(f"/api/frames/1/widgets/{widget_id}/border",
json={"border_style": "solid", "border_thickness": -5, "border_color_index": 0},
headers=csrf_headers(client))
assert resp.status_code == 200, resp.text
assert resp.json()["border_thickness"] == 1 # image_pipeline.MIN_BORDER_THICKNESS
def test_set_border_rejects_unknown_style(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}/border",
json={"border_style": "sparkly", "border_thickness": 3, "border_color_index": 0},
headers=csrf_headers(client))
assert resp.status_code == 400
assert "border_style" in resp.json()["detail"]
def test_set_border_rejects_out_of_range_color_index(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}/border",
json={"border_style": "solid", "border_thickness": 3, "border_color_index": 6},
headers=csrf_headers(client))
assert resp.status_code == 400
assert "border_color_index" in resp.json()["detail"]
def test_set_border_404s_for_unknown_widget(client, db_session):
client.post("/setup", data={"username": "alice", "password": "hunter22"})
resp = client.post("/api/frames/1/widgets/999999/border",
json={"border_style": "solid", "border_thickness": 3, "border_color_index": 0},
headers=csrf_headers(client))
assert resp.status_code == 404
def test_set_border_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}/border",
json={"border_style": "solid", "border_thickness": 3, "border_color_index": 0},
headers=csrf_headers(client))
assert resp.status_code == 404
assert db_session.get(Widget, widget_id).border_style == "none"
def test_set_border_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}/border",
json={"border_style": "solid", "border_thickness": 3, "border_color_index": 0},
headers=csrf_headers(client))
assert resp.status_code == 409
assert resp.json()["detail"]["error"] == "not_controller"
# --- actually renders (real render_panel path, same as /frame/image) ------
def _preview_pixels(client) -> Image.Image:
resp = client.get("/api/frames/1/preview")
assert resp.status_code == 200, resp.text
return Image.open(io.BytesIO(resp.content)).convert("RGB")
def test_solid_border_draws_the_configured_palette_color(client, db_session):
client.post("/setup", data={"username": "alice", "password": "hunter22"})
widget_id = _widget_id(db_session)
# color_index 3 == Red, see image_pipeline.PALETTE_LABELS/DEFAULT_PALETTE_RGB
resp = client.post(f"/api/frames/1/widgets/{widget_id}/border",
json={"border_style": "solid", "border_thickness": 6, "border_color_index": 3},
headers=csrf_headers(client))
assert resp.status_code == 200, resp.text
img = _preview_pixels(client)
assert img.getpixel((0, 0)) == (207, 0, 15) # DEFAULT_PALETTE_RGB[3], top-left corner of the stroke
def test_no_border_leaves_the_edge_unmarked(client, db_session):
"""The default "none" style is a true no-op -- this is the negative
case for test_solid_border_draws_the_configured_palette_color, using
the exact same photo-widget fixture minus the border, so a red edge
pixel there can only be the border, not something else in the
render path already painting it that color."""
client.post("/setup", data={"username": "alice", "password": "hunter22"})
img = _preview_pixels(client)
assert img.getpixel((0, 0)) != (207, 0, 15)