"""app.widgets.weather -- unit-level, no HTTP: constructs Widget/ WeatherWidgetConfig rows directly and monkeypatches the underlying fetch call (get_or_refresh_weather_widget_data, whose own throttle/provider- dispatch logic is covered by test_weather_providers.py and exercised at the HTTP layer in test_widget_config_and_queue_endpoints.py) -- this file is about the widget wiring itself: does render() produce a correctly- sized image for each of the four modes, does it fall back to a placeholder when unconfigured, does check_now force a refetch.""" from __future__ import annotations import time from app import grid, widgets from app.models import Frame, WeatherWidgetConfig, Widget def _make_widget(db_session, **cfg_kwargs) -> tuple[Frame, Widget]: frame = db_session.get(Frame, 1) widget = Widget(frame_id=frame.id, widget_type="weather", x=0, y=0, w=2, h=2, sort_order=0, created_at=time.time()) db_session.add(widget) db_session.flush() db_session.add(WeatherWidgetConfig(widget_id=widget.id, **cfg_kwargs)) db_session.commit() return frame, widget def test_render_falls_back_to_placeholder_when_not_configured(db_session, monkeypatch): frame, widget = _make_widget(db_session, mode="current") monkeypatch.setattr(widgets.weather, "get_or_refresh_weather_widget_data", lambda db, frame, widget: None) img = widgets.weather.render(db_session, frame, widget, 300, 200) assert img.size == (300, 200) assert img.mode == "RGB" def test_render_current_mode(db_session, monkeypatch): frame, widget = _make_widget(db_session, mode="current", city_label="Portland") monkeypatch.setattr(widgets.weather, "get_or_refresh_weather_widget_data", lambda db, frame, widget: {"temp": 72.0, "category": "clear"}) img = widgets.weather.render(db_session, frame, widget, 300, 200) assert img.size == (300, 200) assert img.mode == "RGB" def test_render_hourly_mode(db_session, monkeypatch): frame, widget = _make_widget(db_session, mode="hourly", city_label="Seattle", hourly_interval_hours=4) hourly = [{"time": f"2026-07-27T{h:02d}:00", "temp": 60 + h, "category": "rain"} for h in range(24)] monkeypatch.setattr(widgets.weather, "get_or_refresh_weather_widget_data", lambda db, frame, widget: hourly) img = widgets.weather.render(db_session, frame, widget, 400, 300) assert img.size == (400, 300) def test_render_daily_mode(db_session, monkeypatch): frame, widget = _make_widget(db_session, mode="daily", city_label="Denver", daily_days=5) daily = {f"2026-07-{27 + i}": {"high": 70 + i, "low": 50 + i, "category": "clear"} for i in range(5)} monkeypatch.setattr(widgets.weather, "get_or_refresh_weather_widget_data", lambda db, frame, widget: daily) img = widgets.weather.render(db_session, frame, widget, 400, 300) assert img.size == (400, 300) def test_render_multi_city_mode(db_session, monkeypatch): """cached entries carry the full disambiguated geocoder label (see get_or_refresh_weather_widget_data/weather.geocode_city) -- render() must still produce a correctly-sized image (weather_render. build_multi_city shortens to just the city name for display, same as calendar_render.py's _weather_for_day).""" frame, widget = _make_widget(db_session, mode="multi_city") cities = [ {"label": "Portland, Oregon, United States", "high": 75, "low": 55, "category": "clear"}, {"label": "Seattle, Washington, United States", "high": 65, "low": 50, "category": "rain"}, ] monkeypatch.setattr(widgets.weather, "get_or_refresh_weather_widget_data", lambda db, frame, widget: cities) img = widgets.weather.render(db_session, frame, widget, 400, 300) assert img.size == (400, 300) def test_render_at_minimum_grid_footprint(db_session, monkeypatch): """grid.MIN_FOOTPRINT["weather"] is (2, 2) cells -- on an 8x5 grid against a full 800x480 panel that's a 200x192 box, the smallest a weather widget can actually be placed at.""" assert grid.MIN_FOOTPRINT["weather"] == (2, 2) frame, widget = _make_widget(db_session, mode="current") monkeypatch.setattr(widgets.weather, "get_or_refresh_weather_widget_data", lambda db, frame, widget: {"temp": 72.0, "category": "clear"}) img = widgets.weather.render(db_session, frame, widget, 200, 192) assert img.size == (200, 192) def test_check_now_forces_a_refetch(db_session, monkeypatch): frame, widget = _make_widget(db_session, mode="current") calls = [] def _fake_refresh(db, frame, widget, force=False): calls.append(force) return {"temp": 70.0, "category": "clear"} monkeypatch.setattr(widgets.weather, "get_or_refresh_weather_widget_data", _fake_refresh) widgets.weather.ACTIONS["check_now"](db_session, frame, widget) assert calls == [True] def test_action_labels(): assert set(widgets.weather.ACTIONS.keys()) == {"check_now"} assert widgets.weather.ACTION_LABELS == {"check_now": "Check for updates"}