- Add render_trip_element macro to macros.html; use it in trip_item for the trip list page, giving a consistent one-line-per-element format with emoji, route, times, duration, operator, distance, and CO₂ - Redesign trip_page.html itinerary: day headers use date-only (no year), condense check-out to a single accent line, show time-only on transport cards, humanise duration (Xh Ym), km-only distance, add CO₂ for all transport modes, fix seat display for integer seat values - Fix UndefinedError on /trip/past caused by absent 'arrive' key (Jinja2 Undefined is truthy) and date-only depart/arrive fields (no .date()) - Improve mobile map layout: text column before map in HTML order, reduce mobile map heights, hide toggle button on mobile - Add trips.css with design system (Playfair Display / Source Sans 3 / JetBrains Mono, navy/gold/amber palette, card variants by type) - Add tests/test_trip_list_render.py covering the rendering edge cases Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
188 lines
5.8 KiB
Python
188 lines
5.8 KiB
Python
"""Tests for trip list template rendering.
|
|
|
|
Covers edge cases found in real data that caused UndefinedError / AttributeError:
|
|
1. Flight where 'arrive' key is entirely absent (older data format).
|
|
Jinja2 returns Undefined for missing dict keys, which is truthy, so a
|
|
plain truthiness check on item.arrive would pass and then .date() would
|
|
raise UndefinedError.
|
|
2. Train where depart/arrive are date objects (not datetime).
|
|
datetime.date has no .date() method, so item.depart.date() raises
|
|
AttributeError (surfaced as UndefinedError inside Jinja2).
|
|
"""
|
|
|
|
from datetime import date, datetime, timezone
|
|
|
|
import flask
|
|
import pytest
|
|
|
|
import web_view
|
|
from agenda.types import TripElement
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class _FakeTrip:
|
|
"""Minimal trip stub sufficient for render_trip_element."""
|
|
|
|
show_flags = False
|
|
|
|
|
|
def _render(element: TripElement) -> str:
|
|
"""Render a single TripElement via the render_trip_element macro."""
|
|
with web_view.app.app_context():
|
|
with web_view.app.test_request_context():
|
|
flask.g.user = web_view.User(is_authenticated=False)
|
|
macros = web_view.app.jinja_env.get_template("macros.html")
|
|
return macros.module.render_trip_element(element, _FakeTrip())
|
|
|
|
|
|
def _flight(detail: dict) -> TripElement:
|
|
return TripElement(
|
|
start_time=detail["depart"],
|
|
title="test flight",
|
|
element_type="flight",
|
|
detail=detail,
|
|
start_loc="Origin",
|
|
end_loc="Dest",
|
|
)
|
|
|
|
|
|
def _train(detail: dict) -> TripElement:
|
|
return TripElement(
|
|
start_time=detail["depart"],
|
|
title="test train",
|
|
element_type="train",
|
|
detail=detail,
|
|
start_loc=detail["from"],
|
|
end_loc=detail["to"],
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Unit tests — specific edge cases
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_flight_missing_arrive_key() -> None:
|
|
"""Flight with no 'arrive' key must not raise UndefinedError.
|
|
|
|
Older trips store flights without an arrival time. Jinja2 returns
|
|
Undefined (truthy!) for absent dict keys, so a bare truthiness check
|
|
was insufficient — only 'is defined' correctly guards against it.
|
|
"""
|
|
element = _flight(
|
|
{
|
|
"depart": datetime(2023, 7, 1, 13, 20, tzinfo=timezone.utc),
|
|
# 'arrive' key intentionally absent
|
|
"from": "TIA",
|
|
"to": "LHR",
|
|
"flight_number": "BA123",
|
|
"airline_code": "BA",
|
|
"airline_name": "British Airways",
|
|
"distance": 2000.0,
|
|
}
|
|
)
|
|
result = _render(element)
|
|
assert "Origin" in result
|
|
assert "Dest" in result
|
|
assert "13:05" not in result # arrive time should not appear
|
|
|
|
|
|
def test_flight_arrive_is_none() -> None:
|
|
"""Flight where 'arrive' key exists but value is None must render cleanly."""
|
|
element = _flight(
|
|
{
|
|
"depart": datetime(2023, 7, 1, 13, 20, tzinfo=timezone.utc),
|
|
"arrive": None,
|
|
"from": "TIA",
|
|
"to": "LHR",
|
|
"flight_number": "BA123",
|
|
"airline_code": "BA",
|
|
"airline_name": "British Airways",
|
|
"distance": 2000.0,
|
|
}
|
|
)
|
|
result = _render(element)
|
|
assert "Origin" in result
|
|
|
|
|
|
def test_train_date_only_fields() -> None:
|
|
"""Train with date (not datetime) depart/arrive must not raise AttributeError.
|
|
|
|
Some older train entries record only the travel date with no time.
|
|
datetime.date has no .date() method, so item.depart.date() would fail.
|
|
"""
|
|
element = _train(
|
|
{
|
|
"depart": date(2024, 10, 22),
|
|
"arrive": date(2024, 10, 22),
|
|
"from": "Llandrindod",
|
|
"to": "Llandovery",
|
|
"operator": "Transport for Wales",
|
|
}
|
|
)
|
|
result = _render(element)
|
|
assert "Llandrindod" in result
|
|
assert "Llandovery" in result
|
|
|
|
|
|
def test_train_overnight_datetime() -> None:
|
|
"""Overnight train (arrive on next day) shows night moon emoji and +1 day."""
|
|
tz = timezone.utc
|
|
element = _train(
|
|
{
|
|
"depart": datetime(2026, 1, 19, 19, 6, tzinfo=tz),
|
|
"arrive": datetime(2026, 1, 20, 10, 13, tzinfo=tz),
|
|
"from": "Brussels Midi",
|
|
"to": "Vienna Hbf",
|
|
"operator": "ÖBB",
|
|
}
|
|
)
|
|
result = _render(element)
|
|
assert "🌙" in result
|
|
assert "+1 day" in result
|
|
|
|
|
|
def test_train_seat_as_integer() -> None:
|
|
"""Train with seat as an integer (not a list) must render without error."""
|
|
tz = timezone.utc
|
|
element = _train(
|
|
{
|
|
"depart": datetime(2025, 5, 26, 13, 5, tzinfo=tz),
|
|
"arrive": datetime(2025, 5, 26, 13, 29, tzinfo=tz),
|
|
"from": "Wánchaq",
|
|
"to": "Puno",
|
|
"coach": "C",
|
|
"seat": 1,
|
|
}
|
|
)
|
|
result = _render(element)
|
|
assert "Wánchaq" in result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Integration tests — render the real trip list pages end-to-end
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
"""Flask test client."""
|
|
web_view.app.config["TESTING"] = True
|
|
with web_view.app.test_client() as c:
|
|
yield c
|
|
|
|
|
|
def test_past_trips_page_renders(client) -> None:
|
|
"""Past trips list page renders without any template errors (HTTP 200)."""
|
|
response = client.get("/trip/past")
|
|
assert response.status_code == 200
|
|
|
|
|
|
def test_future_trips_page_renders(client) -> None:
|
|
"""Future trips list page renders without any template errors (HTTP 200)."""
|
|
response = client.get("/trip/future")
|
|
assert response.status_code == 200
|