Phase 2: convert primitives to nodes via a whitelist element factory
Generic leaf builders (Div, Span, Td, Tr, Th, Ul, Li, Strong, Label, Template, P) are now generated from one _html_element factory over the single Element class — the tag name is data, not a per-tag body. Only elements that add classes/behaviour (Button, Pill, Checkbox, Radio, Input, A, SearchField, H1, Modal, AddForm, tables) stay hand-written. All primitives now return Node objects; string-built widgets (Icon, SimpleTable, YearPicker) return Safe, and YearPicker declares its datepicker media. Raw concatenation (_popover_html, Popover slot) uses Fragment. Node.__str__/__html__ now return a SafeString: a node's rendered output is safe HTML by construction, so str(node) stays safe when fed back into a child list or template (matching the old SafeText behaviour and preventing double-escaping). Consumers adapted: the form widgets (SearchSelectWidget, PrimitiveCheckboxWidget) return render(component) so Django gets a safe string; the session form's manual field markup joins via str(row). Component tests render nodes to HTML before asserting. https://claude.ai/code/session_01BKurBhE3Qj25p7Bfsg7EeK
This commit is contained in:
+23
-15
@@ -7,14 +7,30 @@ import django.test
|
||||
from django.utils.safestring import SafeText
|
||||
|
||||
from common.components import (
|
||||
FilterSelect,
|
||||
Pill,
|
||||
SearchSelect,
|
||||
searchselect_selected,
|
||||
)
|
||||
from common.components import FilterSelect as _FilterSelect
|
||||
from common.components import Pill as _Pill
|
||||
from common.components import SearchSelect as _SearchSelect
|
||||
from games.models import Game, Platform
|
||||
|
||||
|
||||
# These components are now lazy nodes; the tests below assert on rendered HTML.
|
||||
# Render at the call site so existing string assertions (assertIn / .count /
|
||||
# .index / .split) keep working, and ``isinstance(..., SafeText)`` confirms the
|
||||
# rendered output is safe markup.
|
||||
def SearchSelect(*args, **kwargs):
|
||||
return str(_SearchSelect(*args, **kwargs))
|
||||
|
||||
|
||||
def FilterSelect(*args, **kwargs):
|
||||
return str(_FilterSelect(*args, **kwargs))
|
||||
|
||||
|
||||
def Pill(*args, **kwargs):
|
||||
return str(_Pill(*args, **kwargs))
|
||||
|
||||
|
||||
class PillTest(unittest.TestCase):
|
||||
def test_returns_safetext(self):
|
||||
self.assertIsInstance(Pill("hi"), SafeText)
|
||||
@@ -201,9 +217,7 @@ class FilterSelectComponentTest(unittest.TestCase):
|
||||
# Both the modifier pill and the value pill render.
|
||||
self.assertIn('data-search-select-modifier="IS_NULL"', html)
|
||||
self.assertIn("(None)", html)
|
||||
self.assertIn(
|
||||
'data-search-select-type="include"', html
|
||||
) # value pill present
|
||||
self.assertIn('data-search-select-type="include"', html) # value pill present
|
||||
self.assertIn('data-modifier="IS_NULL"', html) # container carries it too
|
||||
|
||||
def test_search_url_omits_value_rows_but_keeps_modifiers(self):
|
||||
@@ -250,15 +264,9 @@ class FilterSelectComponentTest(unittest.TestCase):
|
||||
("INCLUDES_ONLY", "(Only)"),
|
||||
],
|
||||
)
|
||||
self.assertIn(
|
||||
'data-search-select-modifier-option="INCLUDES_ALL"', html
|
||||
)
|
||||
self.assertIn(
|
||||
'data-search-select-modifier-option="INCLUDES_ONLY"', html
|
||||
)
|
||||
self.assertIn(
|
||||
'data-search-select-modifier-option="NOT_NULL"', html
|
||||
)
|
||||
self.assertIn('data-search-select-modifier-option="INCLUDES_ALL"', html)
|
||||
self.assertIn('data-search-select-modifier-option="INCLUDES_ONLY"', html)
|
||||
self.assertIn('data-search-select-modifier-option="NOT_NULL"', html)
|
||||
# No legacy match-mode <select>.
|
||||
self.assertNotIn("data-search-select-match", html)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user