"""Rendered-HTML assertions for pages converted to the Python layout/components. These go beyond `test_paths_return_200`: they assert that the `Page()` document wrapper and the Python component bodies emit the right structure, and — most importantly — that nothing is double-escaped (the recurring failure mode when a `SafeText` loses its safe marker and renders as `<tag>`). """ from datetime import datetime from zoneinfo import ZoneInfo from django.conf import settings from django.contrib.auth.models import User from django.test import TestCase from django.urls import reverse from games.models import Game, GameStatusChange, Platform, Purchase, Session ZONEINFO = ZoneInfo(settings.TIME_ZONE) # If any of these appear in output, a SafeText lost its safe marker somewhere. _ESCAPED_TAG_MARKERS = [ "<a", "<div", "<span", "<button", "<input", "<li", ] class RenderedPagesTest(TestCase): def setUp(self) -> None: self.user = User.objects.create_superuser( username="testuser", email="test@example.com", password="testpass" ) self.client.force_login(self.user) self.platform = Platform.objects.create(name="Test Platform", icon="test") self.game = Game.objects.create(name="Test Game", platform=self.platform) self.purchase = Purchase.objects.create( date_purchased=datetime(2022, 9, 26, 14, 58, tzinfo=ZONEINFO), platform=self.platform, ) self.purchase.games.add(self.game) self.session = Session.objects.create( game=self.game, timestamp_start=datetime(2022, 9, 26, 15, 0, tzinfo=ZONEINFO), timestamp_end=datetime(2022, 9, 26, 16, 0, tzinfo=ZONEINFO), ) def get(self, url_name, *args): return self.client.get(reverse(url_name, args=args), follow=True) def assertNoEscapedTags(self, html): for marker in _ESCAPED_TAG_MARKERS: self.assertNotIn( marker, html, f"Found double-escaped markup ({marker!r}) in output" ) # --- layout wrapper ------------------------------------------------------ def test_page_layout_wrapper(self): """A converted page is wrapped in the full Page() document.""" html = self.get("games:list_playevents").content.decode() for marker in [ "", "", ]: self.assertIn(marker, html) self.assertIn("Timetracker - Manage play events", html) # --- list pages ---------------------------------------------------------- def test_list_pages_render_table_unescaped(self): for url_name in [ "games:list_games", "games:list_purchases", "games:list_sessions", "games:list_platforms", "games:list_devices", "games:list_playevents", ]: with self.subTest(url_name=url_name): html = self.get(url_name).content.decode() self.assertIn("", html) self.assertNoEscapedTags(html) def test_add_session_form_has_timestamp_helpers(self): html = self.get("games:add_session").content.decode() self.assertIn("add_session.js", html) for marker in [ "Set to now", "Toggle text", "Copy start value to end", "Copy end value to start", 'data-target="timestamp_start"', 'data-type="now"', 'hx-boost="false"', ]: self.assertIn(marker, html) self.assertNoEscapedTags(html) # --- detail pages -------------------------------------------------------- def test_view_game(self): html = self.get("games:view_game", self.game.id).content.decode() for marker in [ 'id="game-info"', "font-bold font-serif", self.game.name, "Total hours played", # stat popover tooltip 'id="popover-hours"', "Original year", "Status", "Played", "Platform", 'id="history-container"', "status-changed from:body", "createPlayEvent", # the played-row Alpine dropdown script 'hx-target="#global-modal-container"', # delete trigger "Purchases", "Sessions", "Play Events", "History", ]: self.assertIn(marker, html) self.assertNoEscapedTags(html) self.assertEqual(html.count("")) def test_view_game_empty_sections(self): """A game with no sessions/purchases/etc shows the empty messages.""" lonely = Game.objects.create(name="Lonely Game", platform=self.platform) html = self.get("games:view_game", lonely.id).content.decode() for marker in [ "No purchases yet.", "No sessions yet.", "No play events yet.", ]: self.assertIn(marker, html) self.assertNoEscapedTags(html) # --- HTMX fragments ------------------------------------------------------ def test_delete_game_confirmation_modal(self): html = self.get("games:delete_game_confirmation", self.game.id).content.decode() # A fragment (no full-page layout). self.assertNotIn("", html) self.assertIn('id="delete-game-confirmation-modal"', html) self.assertIn("hx-post", html) self.assertIn(self.game.name, html) self.assertIn("session(s)", html) # seeded session self.assertIn("purchase(s)", html) # seeded purchase self.assertNoEscapedTags(html) def test_refund_confirmation_modal(self): html = self.get( "games:refund_purchase_confirmation", self.purchase.id ).content.decode() self.assertIn('id="refund-confirmation-modal"', html) self.assertIn(f"#purchase-row-{self.purchase.id}", html) self.assertIn("Refund", html) self.assertNoEscapedTags(html) def test_session_row_fragment_via_htmx(self): # The inline "finish session" endpoint returns a fragment. resp = self.client.get( reverse("games:list_sessions_end_session", args=[self.session.id]), HTTP_HX_REQUEST="true", ) html = resp.content.decode() self.assertTrue(html.lstrip().startswith("", # full Page() layout "Please log in to continue", "csrfmiddlewaretoken", 'type="submit"', 'value="Login"', "", ]: self.assertIn(marker, html) self.assertIn("Timetracker - Login", html) self.assertNoEscapedTags(html) # --- stats --------------------------------------------------------------- def test_stats_alltime(self): html = self.get("games:stats_alltime").content.decode() for marker in [ 'id="year-picker-input"', "All-time stats", "responsive-table", "Playtime", "Purchases", "Games by playtime", "Platforms by playtime", ]: self.assertIn(marker, html) self.assertNoEscapedTags(html) self.assertEqual(html.count("")) def test_stats_by_year(self): year = self.session.timestamp_start.year html = self.get("games:stats_by_year", year).content.decode() # The seeded game/session/purchase should surface in the year view. self.assertIn("Playtime per month", html) self.assertIn(self.game.name, html) self.assertNoEscapedTags(html) self.assertEqual(html.count("")) def test_view_purchase(self): html = self.get("games:view_purchase", self.purchase.id).content.decode() for marker in [ "dark:text-white max-w-sm", "font-bold font-serif", "Owned on", "Price per game:", "decoration-dotted underline", "Games included in this purchase:", "
    ", "
  • ", ]: self.assertIn(marker, html) self.assertNoEscapedTags(html) # The Python builder emits well-formed, balanced markup. self.assertEqual(html.count(""))