Render nodes explicitly in component tests; drop the proxy/shims
The component tests rendered lazy nodes to HTML through two competing pieces of scaffolding: a magic ``_RenderingComponents.__getattr__`` proxy that auto-str()'d any capitalized builder, plus separate ``str()`` wrapper functions for Checkbox / Radio (test_components) and SearchSelect / FilterSelect / Pill (test_search_select). Replace both with one explicit convention: import the real components and wrap node-returning calls in ``str(...)`` at the call site. ``Node.__str__`` returns a ``SafeText``, so the ``assertIsInstance(..., SafeText)`` checks stay meaningful and every string assertion is unchanged. Non-node helpers (``randomid``, ``_resolve_name_with_icon``, ``_render_element``, the legacy string ``Component()``) are called directly. No production code touched; 141 component/search-select tests and the full 444-test suite pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+153
-128
@@ -2,48 +2,30 @@ import unittest
|
|||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import django
|
import django
|
||||||
|
from django.test import SimpleTestCase
|
||||||
from django.utils.safestring import SafeText, mark_safe
|
from django.utils.safestring import SafeText, mark_safe
|
||||||
|
|
||||||
from common import components as _components
|
from common import components
|
||||||
from common.components.core import Node
|
|
||||||
from games.models import Platform, Game, Purchase, Session
|
from games.models import Platform, Game, Purchase, Session
|
||||||
|
|
||||||
|
# Component builders return lazy ``Node`` objects; these tests assert on rendered
|
||||||
class _RenderingComponents:
|
# HTML, so node-returning calls are wrapped in ``str(...)`` at the call site
|
||||||
"""Test accessor that renders lazy component nodes to safe HTML strings.
|
# (``Node.__str__`` returns a ``SafeText``). Non-node helpers (``randomid``,
|
||||||
|
# ``_resolve_name_with_icon``, the legacy string ``Component()``) are called
|
||||||
Component builders now return ``Node`` objects (the lazy tree). These tests
|
# directly.
|
||||||
assert on rendered HTML, so we render any node a capitalized builder returns
|
|
||||||
to a ``SafeText`` string. Internals (``_render_element``) and the legacy
|
|
||||||
string-returning ``Component()`` are untouched (non-node results pass
|
|
||||||
through), so cache/escaping tests keep working unchanged.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __getattr__(self, name):
|
|
||||||
attr = getattr(_components, name)
|
|
||||||
if not (callable(attr) and name[:1].isupper()):
|
|
||||||
return attr
|
|
||||||
|
|
||||||
def rendered(*args, **kwargs):
|
|
||||||
result = attr(*args, **kwargs)
|
|
||||||
return str(result) if isinstance(result, Node) else result
|
|
||||||
|
|
||||||
return rendered
|
|
||||||
|
|
||||||
|
|
||||||
components = _RenderingComponents()
|
|
||||||
|
|
||||||
|
|
||||||
class ComponentIntegrationTest(unittest.TestCase):
|
class ComponentIntegrationTest(unittest.TestCase):
|
||||||
"""Test Component() works correctly with caching transparent."""
|
"""Test Component() works correctly with caching transparent."""
|
||||||
|
|
||||||
def test_tag_name_component(self):
|
def test_tag_name_component(self):
|
||||||
result = components.Component(
|
result = str(
|
||||||
|
components.Component(
|
||||||
tag_name="div",
|
tag_name="div",
|
||||||
attributes=[("class", "test")],
|
attributes=[("class", "test")],
|
||||||
children="hello",
|
children="hello",
|
||||||
)
|
)
|
||||||
|
)
|
||||||
self.assertEqual(result, '<div class="test">hello</div>')
|
self.assertEqual(result, '<div class="test">hello</div>')
|
||||||
|
|
||||||
|
|
||||||
@@ -54,9 +36,17 @@ class ComponentCacheTest(unittest.TestCase):
|
|||||||
components._render_element.cache_clear()
|
components._render_element.cache_clear()
|
||||||
|
|
||||||
def test_identical_components_hit_cache(self):
|
def test_identical_components_hit_cache(self):
|
||||||
components.Component(tag_name="div", attributes=[("class", "x")], children="hi")
|
str(
|
||||||
|
components.Component(
|
||||||
|
tag_name="div", attributes=[("class", "x")], children="hi"
|
||||||
|
)
|
||||||
|
)
|
||||||
misses = components._render_element.cache_info().misses
|
misses = components._render_element.cache_info().misses
|
||||||
components.Component(tag_name="div", attributes=[("class", "x")], children="hi")
|
str(
|
||||||
|
components.Component(
|
||||||
|
tag_name="div", attributes=[("class", "x")], children="hi"
|
||||||
|
)
|
||||||
|
)
|
||||||
info = components._render_element.cache_info()
|
info = components._render_element.cache_info()
|
||||||
self.assertEqual(info.misses, misses) # no new miss
|
self.assertEqual(info.misses, misses) # no new miss
|
||||||
self.assertGreaterEqual(info.hits, 1) # served from cache
|
self.assertGreaterEqual(info.hits, 1) # served from cache
|
||||||
@@ -67,8 +57,10 @@ class ComponentCacheTest(unittest.TestCase):
|
|||||||
def test_safe_and_unsafe_children_do_not_collide(self):
|
def test_safe_and_unsafe_children_do_not_collide(self):
|
||||||
"""A SafeText "<b>" and a plain "<b>" are equal as strings but must
|
"""A SafeText "<b>" and a plain "<b>" are equal as strings but must
|
||||||
render differently — the cache key must keep them distinct."""
|
render differently — the cache key must keep them distinct."""
|
||||||
safe = components.Component(tag_name="span", children=[mark_safe("<b>x</b>")])
|
safe = str(
|
||||||
unsafe = components.Component(tag_name="span", children=["<b>x</b>"])
|
components.Component(tag_name="span", children=[mark_safe("<b>x</b>")])
|
||||||
|
)
|
||||||
|
unsafe = str(components.Component(tag_name="span", children=["<b>x</b>"]))
|
||||||
self.assertIn("<b>x</b>", safe)
|
self.assertIn("<b>x</b>", safe)
|
||||||
self.assertIn("<b>x</b>", unsafe)
|
self.assertIn("<b>x</b>", unsafe)
|
||||||
self.assertNotEqual(safe, unsafe)
|
self.assertNotEqual(safe, unsafe)
|
||||||
@@ -140,33 +132,37 @@ class PopoverDeterministicTest(unittest.TestCase):
|
|||||||
"""Test that Popover() produces deterministic HTML output."""
|
"""Test that Popover() produces deterministic HTML output."""
|
||||||
|
|
||||||
def test_same_popover_same_id(self):
|
def test_same_popover_same_id(self):
|
||||||
r1 = components.Popover("hello", wrapped_content="hello")
|
r1 = str(components.Popover("hello", wrapped_content="hello"))
|
||||||
r2 = components.Popover("hello", wrapped_content="hello")
|
r2 = str(components.Popover("hello", wrapped_content="hello"))
|
||||||
self.assertEqual(r1, r2)
|
self.assertEqual(r1, r2)
|
||||||
|
|
||||||
def test_different_content_different_id(self):
|
def test_different_content_different_id(self):
|
||||||
r1 = components.Popover("content_a", wrapped_content="content_a")
|
r1 = str(components.Popover("content_a", wrapped_content="content_a"))
|
||||||
r2 = components.Popover("content_b", wrapped_content="content_b")
|
r2 = str(components.Popover("content_b", wrapped_content="content_b"))
|
||||||
self.assertNotEqual(r1, r2)
|
self.assertNotEqual(r1, r2)
|
||||||
|
|
||||||
def test_wrapped_classes_affect_id(self):
|
def test_wrapped_classes_affect_id(self):
|
||||||
r1 = components.Popover("c", wrapped_content="c", wrapped_classes="class_x")
|
r1 = str(
|
||||||
r2 = components.Popover("c", wrapped_content="c", wrapped_classes="class_y")
|
components.Popover("c", wrapped_content="c", wrapped_classes="class_x")
|
||||||
|
)
|
||||||
|
r2 = str(
|
||||||
|
components.Popover("c", wrapped_content="c", wrapped_classes="class_y")
|
||||||
|
)
|
||||||
self.assertNotEqual(r1, r2)
|
self.assertNotEqual(r1, r2)
|
||||||
|
|
||||||
def test_wrapped_content_affects_id(self):
|
def test_wrapped_content_affects_id(self):
|
||||||
r1 = components.Popover("popover", wrapped_content="wrapped_a")
|
r1 = str(components.Popover("popover", wrapped_content="wrapped_a"))
|
||||||
r2 = components.Popover("popover", wrapped_content="wrapped_b")
|
r2 = str(components.Popover("popover", wrapped_content="wrapped_b"))
|
||||||
self.assertNotEqual(r1, r2)
|
self.assertNotEqual(r1, r2)
|
||||||
|
|
||||||
def test_popover_content_affects_id(self):
|
def test_popover_content_affects_id(self):
|
||||||
r1 = components.Popover("popover_a", wrapped_content="wrapped")
|
r1 = str(components.Popover("popover_a", wrapped_content="wrapped"))
|
||||||
r2 = components.Popover("popover_b", wrapped_content="wrapped")
|
r2 = str(components.Popover("popover_b", wrapped_content="wrapped"))
|
||||||
self.assertNotEqual(r1, r2)
|
self.assertNotEqual(r1, r2)
|
||||||
|
|
||||||
def test_full_html_deterministic(self):
|
def test_full_html_deterministic(self):
|
||||||
r1 = components.Popover("hello world", wrapped_content="hello world")
|
r1 = str(components.Popover("hello world", wrapped_content="hello world"))
|
||||||
r2 = components.Popover("hello world", wrapped_content="hello world")
|
r2 = str(components.Popover("hello world", wrapped_content="hello world"))
|
||||||
self.assertEqual(r1.encode(), r2.encode())
|
self.assertEqual(r1.encode(), r2.encode())
|
||||||
|
|
||||||
|
|
||||||
@@ -206,26 +202,26 @@ class ComponentReturnTypeTest(unittest.TestCase):
|
|||||||
"""Test that component functions return SafeText and render correctly."""
|
"""Test that component functions return SafeText and render correctly."""
|
||||||
|
|
||||||
def test_div_returns_safe_text(self):
|
def test_div_returns_safe_text(self):
|
||||||
result = components.Div([("class", "x")], "hello")
|
result = str(components.Div([("class", "x")], "hello"))
|
||||||
self.assertIsInstance(result, SafeText)
|
self.assertIsInstance(result, SafeText)
|
||||||
|
|
||||||
def test_div_deterministic(self):
|
def test_div_deterministic(self):
|
||||||
r1 = components.Div([("class", "x")], "hello")
|
r1 = str(components.Div([("class", "x")], "hello"))
|
||||||
r2 = components.Div([("class", "x")], "hello")
|
r2 = str(components.Div([("class", "x")], "hello"))
|
||||||
self.assertEqual(r1, r2)
|
self.assertEqual(r1, r2)
|
||||||
self.assertIn('<div class="x">hello</div>', r1)
|
self.assertIn('<div class="x">hello</div>', r1)
|
||||||
|
|
||||||
def test_div_no_args(self):
|
def test_div_no_args(self):
|
||||||
result = components.Div(children="test")
|
result = str(components.Div(children="test"))
|
||||||
self.assertIsInstance(result, SafeText)
|
self.assertIsInstance(result, SafeText)
|
||||||
self.assertIn("<div>test</div>", result)
|
self.assertIn("<div>test</div>", result)
|
||||||
|
|
||||||
def test_a_returns_safe_text(self):
|
def test_a_returns_safe_text(self):
|
||||||
result = components.A([], "link")
|
result = str(components.A([], "link"))
|
||||||
self.assertIsInstance(result, SafeText)
|
self.assertIsInstance(result, SafeText)
|
||||||
|
|
||||||
def test_a_literal_href(self):
|
def test_a_literal_href(self):
|
||||||
result = components.A([], "x", href="/literal/path")
|
result = str(components.A([], "x", href="/literal/path"))
|
||||||
self.assertIn('href="/literal/path"', result)
|
self.assertIn('href="/literal/path"', result)
|
||||||
|
|
||||||
def test_a_url_name_reversed(self):
|
def test_a_url_name_reversed(self):
|
||||||
@@ -234,35 +230,35 @@ class ComponentReturnTypeTest(unittest.TestCase):
|
|||||||
with patch(
|
with patch(
|
||||||
"common.components.primitives.reverse", return_value="/resolved/url"
|
"common.components.primitives.reverse", return_value="/resolved/url"
|
||||||
):
|
):
|
||||||
result = components.A([], "link", url_name="some_name")
|
result = str(components.A([], "link", url_name="some_name"))
|
||||||
self.assertIn('href="/resolved/url"', result)
|
self.assertIn('href="/resolved/url"', result)
|
||||||
|
|
||||||
def test_a_no_url_or_href(self):
|
def test_a_no_url_or_href(self):
|
||||||
result = components.A([], "link")
|
result = str(components.A([], "link"))
|
||||||
self.assertIn("<a>link</a>", result)
|
self.assertIn("<a>link</a>", result)
|
||||||
self.assertNotIn("href=", result)
|
self.assertNotIn("href=", result)
|
||||||
|
|
||||||
def test_a_both_url_name_and_href_raises(self):
|
def test_a_both_url_name_and_href_raises(self):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
components.A(href="/path", url_name="some_name")
|
str(components.A(href="/path", url_name="some_name"))
|
||||||
|
|
||||||
def test_button_returns_safe_text(self):
|
def test_button_returns_safe_text(self):
|
||||||
result = components.Button([], "click")
|
result = str(components.Button([], "click"))
|
||||||
self.assertIsInstance(result, SafeText)
|
self.assertIsInstance(result, SafeText)
|
||||||
self.assertIn("<button", result)
|
self.assertIn("<button", result)
|
||||||
|
|
||||||
def test_button_default_colors(self):
|
def test_button_default_colors(self):
|
||||||
result = components.Button([], "click")
|
result = str(components.Button([], "click"))
|
||||||
self.assertIn("text-white bg-brand", result)
|
self.assertIn("text-white bg-brand", result)
|
||||||
|
|
||||||
def test_name_with_icon_no_link(self):
|
def test_name_with_icon_no_link(self):
|
||||||
result = components.NameWithIcon(name="Game", linkify=False)
|
result = str(components.NameWithIcon(name="Game", linkify=False))
|
||||||
self.assertIsInstance(result, SafeText)
|
self.assertIsInstance(result, SafeText)
|
||||||
self.assertIn("Game", result)
|
self.assertIn("Game", result)
|
||||||
self.assertNotIn("<a ", result)
|
self.assertNotIn("<a ", result)
|
||||||
|
|
||||||
def test_name_with_icon_no_trailing_comma(self):
|
def test_name_with_icon_no_trailing_comma(self):
|
||||||
result = components.NameWithIcon(name="Test", linkify=False)
|
result = str(components.NameWithIcon(name="Test", linkify=False))
|
||||||
self.assertIsInstance(result, SafeText)
|
self.assertIsInstance(result, SafeText)
|
||||||
self.assertNotIsInstance(result, tuple)
|
self.assertNotIsInstance(result, tuple)
|
||||||
|
|
||||||
@@ -272,21 +268,23 @@ class ComponentOutputIsNotEscapedTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_component_output_starts_with_tag(self):
|
def test_component_output_starts_with_tag(self):
|
||||||
for label, html in [
|
for label, html in [
|
||||||
("A", components.A(href="/foo", children=["link"])),
|
("A", str(components.A(href="/foo", children=["link"]))),
|
||||||
("Button", components.Button([], "click")),
|
("Button", str(components.Button([], "click"))),
|
||||||
("Div", components.Div([], ["hello"])),
|
("Div", str(components.Div([], ["hello"]))),
|
||||||
("Input", components.Input()),
|
("Input", str(components.Input())),
|
||||||
("ButtonGroup", components.ButtonGroup([])),
|
("ButtonGroup", str(components.ButtonGroup([]))),
|
||||||
(
|
(
|
||||||
"ButtonGroup with buttons",
|
"ButtonGroup with buttons",
|
||||||
|
str(
|
||||||
components.ButtonGroup(
|
components.ButtonGroup(
|
||||||
[{"href": "/", "slot": components.Icon("edit")}]
|
[{"href": "/", "slot": components.Icon("edit")}]
|
||||||
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
("SearchField", components.SearchField()),
|
("SearchField", str(components.SearchField())),
|
||||||
("PriceConverted", components.PriceConverted(["27 CZK"])),
|
("PriceConverted", str(components.PriceConverted(["27 CZK"]))),
|
||||||
("H1", components.H1(["Title"])),
|
("H1", str(components.H1(["Title"]))),
|
||||||
("H1 with badge", components.H1(["Title"], badge="3")),
|
("H1 with badge", str(components.H1(["Title"], badge="3"))),
|
||||||
]:
|
]:
|
||||||
with self.subTest(component=label):
|
with self.subTest(component=label):
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
@@ -295,15 +293,18 @@ class ComponentOutputIsNotEscapedTest(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_button_with_icon_children_not_escaped(self):
|
def test_button_with_icon_children_not_escaped(self):
|
||||||
result = components.Button(
|
result = str(
|
||||||
|
components.Button(
|
||||||
icon=True,
|
icon=True,
|
||||||
size="xs",
|
size="xs",
|
||||||
children=[components.Icon("play"), "LOG"],
|
children=[components.Icon("play"), "LOG"],
|
||||||
)
|
)
|
||||||
|
)
|
||||||
self.assertTrue(str(result).startswith("<button"))
|
self.assertTrue(str(result).startswith("<button"))
|
||||||
|
|
||||||
def test_popover_with_button_children_not_escaped(self):
|
def test_popover_with_button_children_not_escaped(self):
|
||||||
result = components.Popover(
|
result = str(
|
||||||
|
components.Popover(
|
||||||
popover_content="test tooltip",
|
popover_content="test tooltip",
|
||||||
children=[
|
children=[
|
||||||
components.Button(
|
components.Button(
|
||||||
@@ -314,10 +315,11 @@ class ComponentOutputIsNotEscapedTest(unittest.TestCase):
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
)
|
||||||
self.assertTrue(str(result).startswith("<span data-popover-target"))
|
self.assertTrue(str(result).startswith("<span data-popover-target"))
|
||||||
|
|
||||||
def test_name_with_icon_output_not_escaped(self):
|
def test_name_with_icon_output_not_escaped(self):
|
||||||
result = components.NameWithIcon(name="Test", linkify=False)
|
result = str(components.NameWithIcon(name="Test", linkify=False))
|
||||||
self.assertTrue(str(result).startswith("<div"))
|
self.assertTrue(str(result).startswith("<div"))
|
||||||
|
|
||||||
|
|
||||||
@@ -326,59 +328,67 @@ class ComponentEdgeCasesTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_no_tag_name_raises(self):
|
def test_no_tag_name_raises(self):
|
||||||
with self.assertRaises(ValueError) as ctx:
|
with self.assertRaises(ValueError) as ctx:
|
||||||
components.Component(children="hello")
|
str(components.Component(children="hello"))
|
||||||
self.assertIn("tag_name", str(ctx.exception))
|
self.assertIn("tag_name", str(ctx.exception))
|
||||||
|
|
||||||
def test_single_string_children_wrapped(self):
|
def test_single_string_children_wrapped(self):
|
||||||
result = components.Component(tag_name="span", children="hello")
|
result = str(components.Component(tag_name="span", children="hello"))
|
||||||
self.assertIn("hello", result)
|
self.assertIn("hello", result)
|
||||||
|
|
||||||
def test_multiple_children_joined_with_newlines(self):
|
def test_multiple_children_joined_with_newlines(self):
|
||||||
result = components.Component(tag_name="div", children=["hello", "world"])
|
result = str(components.Component(tag_name="div", children=["hello", "world"]))
|
||||||
self.assertIn("hello\nworld", result)
|
self.assertIn("hello\nworld", result)
|
||||||
self.assertIn("<div>", result)
|
self.assertIn("<div>", result)
|
||||||
self.assertIn("</div>", result)
|
self.assertIn("</div>", result)
|
||||||
|
|
||||||
def test_raw_html_children_are_escaped(self):
|
def test_raw_html_children_are_escaped(self):
|
||||||
result = components.Component(
|
result = str(
|
||||||
|
components.Component(
|
||||||
tag_name="div", children=["<script>alert('xss')</script>"]
|
tag_name="div", children=["<script>alert('xss')</script>"]
|
||||||
)
|
)
|
||||||
|
)
|
||||||
self.assertNotIn("<script>", result)
|
self.assertNotIn("<script>", result)
|
||||||
self.assertIn("<script>", result)
|
self.assertIn("<script>", result)
|
||||||
|
|
||||||
def test_mark_safe_children_pass_through(self):
|
def test_mark_safe_children_pass_through(self):
|
||||||
result = components.Component(
|
result = str(
|
||||||
|
components.Component(
|
||||||
tag_name="div", children=[mark_safe("<span>safe</span>")]
|
tag_name="div", children=[mark_safe("<span>safe</span>")]
|
||||||
)
|
)
|
||||||
|
)
|
||||||
self.assertIn("<span>safe</span>", result)
|
self.assertIn("<span>safe</span>", result)
|
||||||
|
|
||||||
def test_attribute_values_are_escaped(self):
|
def test_attribute_values_are_escaped(self):
|
||||||
result = components.Component(
|
result = str(
|
||||||
|
components.Component(
|
||||||
tag_name="div",
|
tag_name="div",
|
||||||
attributes=[("data-x", 'foo"bar')],
|
attributes=[("data-x", 'foo"bar')],
|
||||||
)
|
)
|
||||||
|
)
|
||||||
self.assertIn(""", result)
|
self.assertIn(""", result)
|
||||||
self.assertNotIn('"foo"bar"', result)
|
self.assertNotIn('"foo"bar"', result)
|
||||||
|
|
||||||
def test_attributes_serialized_correctly(self):
|
def test_attributes_serialized_correctly(self):
|
||||||
result = components.Component(
|
result = str(
|
||||||
|
components.Component(
|
||||||
tag_name="div", attributes=[("class", "foo"), ("id", "bar")]
|
tag_name="div", attributes=[("class", "foo"), ("id", "bar")]
|
||||||
)
|
)
|
||||||
|
)
|
||||||
self.assertIn('class="foo"', result)
|
self.assertIn('class="foo"', result)
|
||||||
self.assertIn('id="bar"', result)
|
self.assertIn('id="bar"', result)
|
||||||
|
|
||||||
def test_empty_attributes_no_extra_space(self):
|
def test_empty_attributes_no_extra_space(self):
|
||||||
result = components.Component(tag_name="span", children="x")
|
result = str(components.Component(tag_name="span", children="x"))
|
||||||
self.assertEqual(result, "<span>x</span>")
|
self.assertEqual(result, "<span>x</span>")
|
||||||
self.assertNotIn(" <span", result)
|
self.assertNotIn(" <span", result)
|
||||||
|
|
||||||
def test_non_string_children_not_supported(self):
|
def test_non_string_children_not_supported(self):
|
||||||
"""Component only accepts str for children, not integers."""
|
"""Component only accepts str for children, not integers."""
|
||||||
result = components.Component(tag_name="span", children=str(42))
|
result = str(components.Component(tag_name="span", children=str(42)))
|
||||||
self.assertIn("42", result)
|
self.assertIn("42", result)
|
||||||
|
|
||||||
def test_returns_safetext(self):
|
def test_returns_safetext(self):
|
||||||
result = components.Component(tag_name="div", children="test")
|
result = str(components.Component(tag_name="div", children="test"))
|
||||||
self.assertIsInstance(result, SafeText)
|
self.assertIsInstance(result, SafeText)
|
||||||
|
|
||||||
|
|
||||||
@@ -386,22 +396,22 @@ class IconTest(unittest.TestCase):
|
|||||||
"""Test Icon() component function."""
|
"""Test Icon() component function."""
|
||||||
|
|
||||||
def test_valid_icon_renders_svg(self):
|
def test_valid_icon_renders_svg(self):
|
||||||
result = components.Icon("play")
|
result = str(components.Icon("play"))
|
||||||
self.assertIsInstance(result, SafeText)
|
self.assertIsInstance(result, SafeText)
|
||||||
self.assertIn("<svg", result)
|
self.assertIn("<svg", result)
|
||||||
self.assertIn("</svg>", result)
|
self.assertIn("</svg>", result)
|
||||||
|
|
||||||
def test_unavailable_icon_falls_back(self):
|
def test_unavailable_icon_falls_back(self):
|
||||||
result = components.Icon("zzz_nonexistent_platform")
|
result = str(components.Icon("zzz_nonexistent_platform"))
|
||||||
self.assertIsInstance(result, SafeText)
|
self.assertIsInstance(result, SafeText)
|
||||||
self.assertIn("<svg", result)
|
self.assertIn("<svg", result)
|
||||||
|
|
||||||
def test_icon_passes_attributes_to_template(self):
|
def test_icon_passes_attributes_to_template(self):
|
||||||
result = components.Icon("play", attributes=[("title", "Play")])
|
result = str(components.Icon("play", attributes=[("title", "Play")]))
|
||||||
self.assertIsInstance(result, SafeText)
|
self.assertIsInstance(result, SafeText)
|
||||||
|
|
||||||
def test_returns_safetext(self):
|
def test_returns_safetext(self):
|
||||||
result = components.Icon("delete")
|
result = str(components.Icon("delete"))
|
||||||
self.assertIsInstance(result, SafeText)
|
self.assertIsInstance(result, SafeText)
|
||||||
|
|
||||||
|
|
||||||
@@ -409,18 +419,20 @@ class InputTest(unittest.TestCase):
|
|||||||
"""Test the Input() component."""
|
"""Test the Input() component."""
|
||||||
|
|
||||||
def test_input_default_type_text(self):
|
def test_input_default_type_text(self):
|
||||||
result = components.Input()
|
result = str(components.Input())
|
||||||
self.assertIn("<input", result)
|
self.assertIn("<input", result)
|
||||||
self.assertIn('type="text"', result)
|
self.assertIn('type="text"', result)
|
||||||
|
|
||||||
def test_input_custom_type(self):
|
def test_input_custom_type(self):
|
||||||
result = components.Input(type="submit")
|
result = str(components.Input(type="submit"))
|
||||||
self.assertIn('type="submit"', result)
|
self.assertIn('type="submit"', result)
|
||||||
|
|
||||||
def test_input_attributes_merged_with_type(self):
|
def test_input_attributes_merged_with_type(self):
|
||||||
result = components.Input(
|
result = str(
|
||||||
|
components.Input(
|
||||||
type="email", attributes=[("id", "email"), ("class", "form-input")]
|
type="email", attributes=[("id", "email"), ("class", "form-input")]
|
||||||
)
|
)
|
||||||
|
)
|
||||||
self.assertIn('type="email"', result)
|
self.assertIn('type="email"', result)
|
||||||
self.assertIn('id="email"', result)
|
self.assertIn('id="email"', result)
|
||||||
self.assertIn('class="form-input"', result)
|
self.assertIn('class="form-input"', result)
|
||||||
@@ -430,12 +442,12 @@ class PopoverTruncatedTest(unittest.TestCase):
|
|||||||
"""Test PopoverTruncated() component function."""
|
"""Test PopoverTruncated() component function."""
|
||||||
|
|
||||||
def test_short_string_no_popover(self):
|
def test_short_string_no_popover(self):
|
||||||
result = components.PopoverTruncated("hi")
|
result = str(components.PopoverTruncated("hi"))
|
||||||
self.assertEqual(result, "hi")
|
self.assertEqual(result, "hi")
|
||||||
|
|
||||||
def test_long_string_wrapped_in_popover(self):
|
def test_long_string_wrapped_in_popover(self):
|
||||||
long_text = "a" * 100
|
long_text = "a" * 100
|
||||||
result = components.PopoverTruncated(long_text)
|
result = str(components.PopoverTruncated(long_text))
|
||||||
# Should NOT equal the truncated form directly
|
# Should NOT equal the truncated form directly
|
||||||
truncated = components.truncate(long_text, 30)
|
truncated = components.truncate(long_text, 30)
|
||||||
self.assertNotEqual(result, truncated)
|
self.assertNotEqual(result, truncated)
|
||||||
@@ -444,47 +456,55 @@ class PopoverTruncatedTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_custom_ellipsis_used(self):
|
def test_custom_ellipsis_used(self):
|
||||||
long_text = "a" * 50
|
long_text = "a" * 50
|
||||||
result = components.PopoverTruncated(long_text, ellipsis=">>")
|
result = str(components.PopoverTruncated(long_text, ellipsis=">>"))
|
||||||
# Django template escapes >> to >> in the wrapped_content
|
# Django template escapes >> to >> in the wrapped_content
|
||||||
self.assertIn(">>", result)
|
self.assertIn(">>", result)
|
||||||
|
|
||||||
def test_popover_if_not_truncated_flag(self):
|
def test_popover_if_not_truncated_flag(self):
|
||||||
short_text = "hi"
|
short_text = "hi"
|
||||||
result = components.PopoverTruncated(
|
result = str(
|
||||||
short_text, popover_content="full content", popover_if_not_truncated=True
|
components.PopoverTruncated(
|
||||||
|
short_text,
|
||||||
|
popover_content="full content",
|
||||||
|
popover_if_not_truncated=True,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
# Should be wrapped in popover even though short
|
# Should be wrapped in popover even though short
|
||||||
self.assertNotEqual(result, "hi")
|
self.assertNotEqual(result, "hi")
|
||||||
self.assertIn("data-popover-target", result)
|
self.assertIn("data-popover-target", result)
|
||||||
|
|
||||||
def test_popover_content_override(self):
|
def test_popover_content_override(self):
|
||||||
result = components.PopoverTruncated("short", popover_content="custom popover")
|
result = str(
|
||||||
|
components.PopoverTruncated("short", popover_content="custom popover")
|
||||||
|
)
|
||||||
# With popover_if_not_truncated=False (default), short text returns as-is
|
# With popover_if_not_truncated=False (default), short text returns as-is
|
||||||
self.assertEqual(result, "short")
|
self.assertEqual(result, "short")
|
||||||
|
|
||||||
def test_popover_content_override_with_flag(self):
|
def test_popover_content_override_with_flag(self):
|
||||||
result = components.PopoverTruncated(
|
result = str(
|
||||||
|
components.PopoverTruncated(
|
||||||
"short", popover_content="custom popover", popover_if_not_truncated=True
|
"short", popover_content="custom popover", popover_if_not_truncated=True
|
||||||
)
|
)
|
||||||
|
)
|
||||||
self.assertIn("custom popover", result)
|
self.assertIn("custom popover", result)
|
||||||
|
|
||||||
def test_endpart_visible_in_output(self):
|
def test_endpart_visible_in_output(self):
|
||||||
long_text = "a" * 50
|
long_text = "a" * 50
|
||||||
result = components.PopoverTruncated(long_text, endpart="...")
|
result = str(components.PopoverTruncated(long_text, endpart="..."))
|
||||||
self.assertIn("...", result)
|
self.assertIn("...", result)
|
||||||
|
|
||||||
def test_returns_safetext(self):
|
def test_returns_safetext(self):
|
||||||
result = components.PopoverTruncated("a" * 100)
|
result = str(components.PopoverTruncated("a" * 100))
|
||||||
self.assertIsInstance(result, SafeText)
|
self.assertIsInstance(result, SafeText)
|
||||||
|
|
||||||
def test_default_length(self):
|
def test_default_length(self):
|
||||||
text = "a" * 31
|
text = "a" * 31
|
||||||
result = components.PopoverTruncated(text)
|
result = str(components.PopoverTruncated(text))
|
||||||
# 31 chars exceeds default length of 30, so should be truncated
|
# 31 chars exceeds default length of 30, so should be truncated
|
||||||
self.assertIn("data-popover-target", result)
|
self.assertIn("data-popover-target", result)
|
||||||
|
|
||||||
def test_length_zero(self):
|
def test_length_zero(self):
|
||||||
result = components.PopoverTruncated("hello", length=0)
|
result = str(components.PopoverTruncated("hello", length=0))
|
||||||
# Even empty length triggers popover for any content
|
# Even empty length triggers popover for any content
|
||||||
self.assertIn("data-popover-target", result)
|
self.assertIn("data-popover-target", result)
|
||||||
|
|
||||||
@@ -516,7 +536,7 @@ class ModelDependentComponentsTest(django.test.TestCase):
|
|||||||
def test_name_with_icon_linkify_with_game(self):
|
def test_name_with_icon_linkify_with_game(self):
|
||||||
platform = self._create_platform(name="Steam", icon="steam")
|
platform = self._create_platform(name="Steam", icon="steam")
|
||||||
game = self._create_game(platform)
|
game = self._create_game(platform)
|
||||||
result = components.NameWithIcon(game=game, linkify=True)
|
result = str(components.NameWithIcon(game=game, linkify=True))
|
||||||
self.assertIsInstance(result, SafeText)
|
self.assertIsInstance(result, SafeText)
|
||||||
self.assertIn("<a ", result)
|
self.assertIn("<a ", result)
|
||||||
self.assertIn("Test Game", result)
|
self.assertIn("Test Game", result)
|
||||||
@@ -525,7 +545,9 @@ class ModelDependentComponentsTest(django.test.TestCase):
|
|||||||
def test_name_with_icon_no_linkify(self):
|
def test_name_with_icon_no_linkify(self):
|
||||||
platform = self._create_platform(name="GOG", icon="gog")
|
platform = self._create_platform(name="GOG", icon="gog")
|
||||||
game = self._create_game(platform)
|
game = self._create_game(platform)
|
||||||
result = components.NameWithIcon(name="Test Game", game=game, linkify=False)
|
result = str(
|
||||||
|
components.NameWithIcon(name="Test Game", game=game, linkify=False)
|
||||||
|
)
|
||||||
self.assertIsInstance(result, SafeText)
|
self.assertIsInstance(result, SafeText)
|
||||||
self.assertNotIn("<a ", result)
|
self.assertNotIn("<a ", result)
|
||||||
self.assertIn("Test Game", result)
|
self.assertIn("Test Game", result)
|
||||||
@@ -538,13 +560,13 @@ class ModelDependentComponentsTest(django.test.TestCase):
|
|||||||
timestamp_start="2025-01-01 00:00:00+00:00",
|
timestamp_start="2025-01-01 00:00:00+00:00",
|
||||||
emulated=True,
|
emulated=True,
|
||||||
)
|
)
|
||||||
result = components.NameWithIcon(session=session, linkify=True)
|
result = str(components.NameWithIcon(session=session, linkify=True))
|
||||||
self.assertIsInstance(result, SafeText)
|
self.assertIsInstance(result, SafeText)
|
||||||
self.assertIn("<a ", result)
|
self.assertIn("<a ", result)
|
||||||
self.assertIn("Emulated", result)
|
self.assertIn("Emulated", result)
|
||||||
|
|
||||||
def test_name_with_icon_no_platform(self):
|
def test_name_with_icon_no_platform(self):
|
||||||
result = components.NameWithIcon(name="Standalone", linkify=False)
|
result = str(components.NameWithIcon(name="Standalone", linkify=False))
|
||||||
self.assertIsInstance(result, SafeText)
|
self.assertIsInstance(result, SafeText)
|
||||||
self.assertIn("Standalone", result)
|
self.assertIn("Standalone", result)
|
||||||
|
|
||||||
@@ -555,7 +577,7 @@ class ModelDependentComponentsTest(django.test.TestCase):
|
|||||||
game=game,
|
game=game,
|
||||||
timestamp_start="2025-01-01 00:00:00+00:00",
|
timestamp_start="2025-01-01 00:00:00+00:00",
|
||||||
)
|
)
|
||||||
result = components.NameWithIcon(session=session, linkify=True)
|
result = str(components.NameWithIcon(session=session, linkify=True))
|
||||||
self.assertIsInstance(result, SafeText)
|
self.assertIsInstance(result, SafeText)
|
||||||
self.assertIn("Epic Game", result)
|
self.assertIn("Epic Game", result)
|
||||||
|
|
||||||
@@ -563,7 +585,7 @@ class ModelDependentComponentsTest(django.test.TestCase):
|
|||||||
platform = self._create_platform()
|
platform = self._create_platform()
|
||||||
game = self._create_game(platform)
|
game = self._create_game(platform)
|
||||||
purchase = self._create_purchase([game], price=29.99)
|
purchase = self._create_purchase([game], price=29.99)
|
||||||
result = components.PurchasePrice(purchase)
|
result = str(components.PurchasePrice(purchase))
|
||||||
self.assertIsInstance(result, SafeText)
|
self.assertIsInstance(result, SafeText)
|
||||||
# floatformat rounds to 1 decimal: 29.99 -> 30.0
|
# floatformat rounds to 1 decimal: 29.99 -> 30.0
|
||||||
self.assertIn("30.0", result)
|
self.assertIn("30.0", result)
|
||||||
@@ -574,7 +596,7 @@ class ModelDependentComponentsTest(django.test.TestCase):
|
|||||||
platform = self._create_platform(icon="steam")
|
platform = self._create_platform(icon="steam")
|
||||||
game = self._create_game(platform, name="Single Game")
|
game = self._create_game(platform, name="Single Game")
|
||||||
purchase = self._create_purchase([game], price=14.99)
|
purchase = self._create_purchase([game], price=14.99)
|
||||||
result = components.LinkedPurchase(purchase)
|
result = str(components.LinkedPurchase(purchase))
|
||||||
self.assertIsInstance(result, SafeText)
|
self.assertIsInstance(result, SafeText)
|
||||||
self.assertIn("Single Game", result)
|
self.assertIn("Single Game", result)
|
||||||
self.assertIn("<a ", result)
|
self.assertIn("<a ", result)
|
||||||
@@ -585,7 +607,7 @@ class ModelDependentComponentsTest(django.test.TestCase):
|
|||||||
game1 = self._create_game(platform, name="Game One")
|
game1 = self._create_game(platform, name="Game One")
|
||||||
game2 = self._create_game(platform, name="Game Two")
|
game2 = self._create_game(platform, name="Game Two")
|
||||||
purchase = self._create_purchase([game1, game2], price=24.99)
|
purchase = self._create_purchase([game1, game2], price=24.99)
|
||||||
result = components.LinkedPurchase(purchase)
|
result = str(components.LinkedPurchase(purchase))
|
||||||
self.assertIsInstance(result, SafeText)
|
self.assertIsInstance(result, SafeText)
|
||||||
self.assertIn("2 games", result)
|
self.assertIn("2 games", result)
|
||||||
self.assertIn("<a ", result)
|
self.assertIn("<a ", result)
|
||||||
@@ -601,7 +623,7 @@ class ModelDependentComponentsTest(django.test.TestCase):
|
|||||||
)
|
)
|
||||||
purchase.name = "Bundle"
|
purchase.name = "Bundle"
|
||||||
purchase.save()
|
purchase.save()
|
||||||
result = components.LinkedPurchase(purchase)
|
result = str(components.LinkedPurchase(purchase))
|
||||||
self.assertIsInstance(result, SafeText)
|
self.assertIsInstance(result, SafeText)
|
||||||
self.assertIn("Bundle", result)
|
self.assertIn("Bundle", result)
|
||||||
|
|
||||||
@@ -610,7 +632,7 @@ class ModelDependentComponentsTest(django.test.TestCase):
|
|||||||
game1 = self._create_game(platform, name="Alpha")
|
game1 = self._create_game(platform, name="Alpha")
|
||||||
game2 = self._create_game(platform, name="Beta")
|
game2 = self._create_game(platform, name="Beta")
|
||||||
purchase = self._create_purchase([game1, game2], price=19.99)
|
purchase = self._create_purchase([game1, game2], price=19.99)
|
||||||
result = components.LinkedPurchase(purchase)
|
result = str(components.LinkedPurchase(purchase))
|
||||||
self.assertIsInstance(result, SafeText)
|
self.assertIsInstance(result, SafeText)
|
||||||
self.assertIn("Alpha", result)
|
self.assertIn("Alpha", result)
|
||||||
self.assertIn("Beta", result)
|
self.assertIn("Beta", result)
|
||||||
@@ -621,18 +643,18 @@ class PurchaseTruncatedTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_endpart_shorter_than_length(self):
|
def test_endpart_shorter_than_length(self):
|
||||||
text = "a" * 50
|
text = "a" * 50
|
||||||
result = components.PopoverTruncated(text, length=10, endpart="x")
|
result = str(components.PopoverTruncated(text, length=10, endpart="x"))
|
||||||
# endpart=x takes 1 char, so content gets truncated at 9 chars
|
# endpart=x takes 1 char, so content gets truncated at 9 chars
|
||||||
self.assertIn("data-popover-target", result)
|
self.assertIn("data-popover-target", result)
|
||||||
self.assertIn("x", result)
|
self.assertIn("x", result)
|
||||||
|
|
||||||
def test_no_truncation_no_ellipsis(self):
|
def test_no_truncation_no_ellipsis(self):
|
||||||
result = components.PopoverTruncated("short text")
|
result = str(components.PopoverTruncated("short text"))
|
||||||
self.assertEqual(result, "short text")
|
self.assertEqual(result, "short text")
|
||||||
|
|
||||||
def test_custom_length(self):
|
def test_custom_length(self):
|
||||||
text = "hello world"
|
text = "hello world"
|
||||||
result = components.PopoverTruncated(text, length=6)
|
result = str(components.PopoverTruncated(text, length=6))
|
||||||
self.assertIn("data-popover-target", result)
|
self.assertIn("data-popover-target", result)
|
||||||
|
|
||||||
|
|
||||||
@@ -646,12 +668,14 @@ class NameWithIconPlatformTest(django.test.TestCase):
|
|||||||
cls.game = Game.objects.create(name="Zelda", platform=cls.platform)
|
cls.game = Game.objects.create(name="Zelda", platform=cls.platform)
|
||||||
|
|
||||||
def test_name_with_icon_shows_platform_icon(self):
|
def test_name_with_icon_shows_platform_icon(self):
|
||||||
result = components.NameWithIcon(name="Zelda", game=self.game, linkify=True)
|
result = str(
|
||||||
|
components.NameWithIcon(name="Zelda", game=self.game, linkify=True)
|
||||||
|
)
|
||||||
self.assertIsInstance(result, SafeText)
|
self.assertIsInstance(result, SafeText)
|
||||||
self.assertIn("Zelda", result)
|
self.assertIn("Zelda", result)
|
||||||
|
|
||||||
def test_name_with_icon_no_game_id_no_platform(self):
|
def test_name_with_icon_no_game_id_no_platform(self):
|
||||||
result = components.NameWithIcon(name="Unknown Game", linkify=False)
|
result = str(components.NameWithIcon(name="Unknown Game", linkify=False))
|
||||||
self.assertIsInstance(result, SafeText)
|
self.assertIsInstance(result, SafeText)
|
||||||
self.assertIn("Unknown Game", result)
|
self.assertIn("Unknown Game", result)
|
||||||
|
|
||||||
@@ -775,11 +799,13 @@ class SimpleTableRenderingTest(unittest.TestCase):
|
|||||||
def test_simple_table_renders_list_rows(self):
|
def test_simple_table_renders_list_rows(self):
|
||||||
"""Verify list-style rows render as <tr> with <th scope='row'> + <td>."""
|
"""Verify list-style rows render as <tr> with <th scope='row'> + <td>."""
|
||||||
result = str(
|
result = str(
|
||||||
|
str(
|
||||||
components.SimpleTable(
|
components.SimpleTable(
|
||||||
columns=["Game", "Started", "Ended"],
|
columns=["Game", "Started", "Ended"],
|
||||||
rows=[["Game1", "2025-01-01", "2025-03-01"]],
|
rows=[["Game1", "2025-01-01", "2025-03-01"]],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
tbody = self._tbody(result)
|
tbody = self._tbody(result)
|
||||||
self.assertIn("<tr", tbody)
|
self.assertIn("<tr", tbody)
|
||||||
self.assertIn("Game1", tbody)
|
self.assertIn("Game1", tbody)
|
||||||
@@ -800,11 +826,13 @@ class SimpleTableRenderingTest(unittest.TestCase):
|
|||||||
def test_simple_table_multiple_rows(self):
|
def test_simple_table_multiple_rows(self):
|
||||||
"""Verify multiple rows all render."""
|
"""Verify multiple rows all render."""
|
||||||
result = str(
|
result = str(
|
||||||
|
str(
|
||||||
components.SimpleTable(
|
components.SimpleTable(
|
||||||
columns=["Game", "Started"],
|
columns=["Game", "Started"],
|
||||||
rows=[["GameA", "2025-01-01"], ["GameB", "2025-02-01"]],
|
rows=[["GameA", "2025-01-01"], ["GameB", "2025-02-01"]],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
tbody = self._tbody(result)
|
tbody = self._tbody(result)
|
||||||
self.assertIn("GameA", tbody)
|
self.assertIn("GameA", tbody)
|
||||||
self.assertIn("GameB", tbody)
|
self.assertIn("GameB", tbody)
|
||||||
@@ -815,12 +843,14 @@ class SimpleTableRenderingTest(unittest.TestCase):
|
|||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
result = str(
|
result = str(
|
||||||
|
str(
|
||||||
components.SimpleTable(
|
components.SimpleTable(
|
||||||
columns=["Game", "Started"],
|
columns=["Game", "Started"],
|
||||||
rows=[["Game1", "2025-01-01"]],
|
rows=[["Game1", "2025-01-01"]],
|
||||||
header_action=mark_safe('<a href="/add">Add</a>'),
|
header_action=mark_safe('<a href="/add">Add</a>'),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
self.assertIn("<caption", result)
|
self.assertIn("<caption", result)
|
||||||
self.assertIn('href="/add"', result)
|
self.assertIn('href="/add"', result)
|
||||||
self.assertIn(">Add</", result)
|
self.assertIn(">Add</", result)
|
||||||
@@ -828,6 +858,7 @@ class SimpleTableRenderingTest(unittest.TestCase):
|
|||||||
def test_simple_table_dict_rows_with_cell_data(self):
|
def test_simple_table_dict_rows_with_cell_data(self):
|
||||||
"""Verify dict-style rows with row_id and cell_data render correctly."""
|
"""Verify dict-style rows with row_id and cell_data render correctly."""
|
||||||
result = str(
|
result = str(
|
||||||
|
str(
|
||||||
components.SimpleTable(
|
components.SimpleTable(
|
||||||
columns=["Name", "Date"],
|
columns=["Name", "Date"],
|
||||||
rows=[
|
rows=[
|
||||||
@@ -839,6 +870,7 @@ class SimpleTableRenderingTest(unittest.TestCase):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
tbody = self._tbody(result)
|
tbody = self._tbody(result)
|
||||||
self.assertIn('id="session-row-1"', tbody)
|
self.assertIn('id="session-row-1"', tbody)
|
||||||
self.assertIn("device-changed", tbody)
|
self.assertIn("device-changed", tbody)
|
||||||
@@ -847,24 +879,13 @@ class SimpleTableRenderingTest(unittest.TestCase):
|
|||||||
self.assertIn("2025-01-01", tbody)
|
self.assertIn("2025-01-01", tbody)
|
||||||
|
|
||||||
|
|
||||||
from django.test import SimpleTestCase
|
|
||||||
from common.components.primitives import Checkbox as _Checkbox, Radio as _Radio
|
|
||||||
|
|
||||||
|
|
||||||
# Checkbox/Radio are lazy nodes; render to safe HTML for the assertions below.
|
|
||||||
def Checkbox(*args, **kwargs):
|
|
||||||
return str(_Checkbox(*args, **kwargs))
|
|
||||||
|
|
||||||
|
|
||||||
def Radio(*args, **kwargs):
|
|
||||||
return str(_Radio(*args, **kwargs))
|
|
||||||
|
|
||||||
|
|
||||||
class ComponentPrimitivesTest(SimpleTestCase):
|
class ComponentPrimitivesTest(SimpleTestCase):
|
||||||
def test_checkbox_primitive(self):
|
def test_checkbox_primitive(self):
|
||||||
html = Checkbox(
|
html = str(
|
||||||
|
components.Checkbox(
|
||||||
name="test-check", label="Accept Terms", checked=True, value="yes"
|
name="test-check", label="Accept Terms", checked=True, value="yes"
|
||||||
)
|
)
|
||||||
|
)
|
||||||
self.assertIn('type="checkbox"', html)
|
self.assertIn('type="checkbox"', html)
|
||||||
self.assertIn('name="test-check"', html)
|
self.assertIn('name="test-check"', html)
|
||||||
self.assertIn('value="yes"', html)
|
self.assertIn('value="yes"', html)
|
||||||
@@ -872,14 +893,18 @@ class ComponentPrimitivesTest(SimpleTestCase):
|
|||||||
self.assertIn("Accept Terms", html)
|
self.assertIn("Accept Terms", html)
|
||||||
|
|
||||||
def test_checkbox_headless(self):
|
def test_checkbox_headless(self):
|
||||||
html = Checkbox(name="test-headless", label=None, checked=True)
|
html = str(components.Checkbox(name="test-headless", label=None, checked=True))
|
||||||
self.assertNotIn("<label", html)
|
self.assertNotIn("<label", html)
|
||||||
self.assertIn("<input", html)
|
self.assertIn("<input", html)
|
||||||
self.assertIn('type="checkbox"', html)
|
self.assertIn('type="checkbox"', html)
|
||||||
self.assertIn('name="test-headless"', html)
|
self.assertIn('name="test-headless"', html)
|
||||||
|
|
||||||
def test_radio_primitive(self):
|
def test_radio_primitive(self):
|
||||||
html = Radio(name="test-radio", label="Option A", checked=False, value="A")
|
html = str(
|
||||||
|
components.Radio(
|
||||||
|
name="test-radio", label="Option A", checked=False, value="A"
|
||||||
|
)
|
||||||
|
)
|
||||||
self.assertIn('type="radio"', html)
|
self.assertIn('type="radio"', html)
|
||||||
self.assertIn('name="test-radio"', html)
|
self.assertIn('name="test-radio"', html)
|
||||||
self.assertIn('value="A"', html)
|
self.assertIn('value="A"', html)
|
||||||
|
|||||||
+59
-48
@@ -9,83 +9,74 @@ from django.utils.safestring import SafeText
|
|||||||
from common.components import (
|
from common.components import (
|
||||||
searchselect_selected,
|
searchselect_selected,
|
||||||
)
|
)
|
||||||
from common.components import FilterSelect as _FilterSelect
|
from common.components import FilterSelect, Pill, SearchSelect
|
||||||
from common.components import Pill as _Pill
|
|
||||||
from common.components import SearchSelect as _SearchSelect
|
|
||||||
from games.models import Game, Platform
|
from games.models import Game, Platform
|
||||||
|
|
||||||
|
# These components are lazy nodes; the tests below assert on rendered HTML, so
|
||||||
# These components are now lazy nodes; the tests below assert on rendered HTML.
|
# each call is wrapped in ``str(...)`` (``Node.__str__`` returns a ``SafeText``,
|
||||||
# Render at the call site so existing string assertions (assertIn / .count /
|
# which keeps the ``assertIsInstance(..., SafeText)`` checks meaningful and the
|
||||||
# .index / .split) keep working, and ``isinstance(..., SafeText)`` confirms the
|
# string assertions working).
|
||||||
# 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):
|
class PillTest(unittest.TestCase):
|
||||||
def test_returns_safetext(self):
|
def test_returns_safetext(self):
|
||||||
self.assertIsInstance(Pill("hi"), SafeText)
|
self.assertIsInstance(str(Pill("hi")), SafeText)
|
||||||
|
|
||||||
def test_plain_pill_has_data_pill_no_remove(self):
|
def test_plain_pill_has_data_pill_no_remove(self):
|
||||||
html = Pill("hi")
|
html = str(Pill("hi"))
|
||||||
self.assertIn("data-pill", html)
|
self.assertIn("data-pill", html)
|
||||||
self.assertNotIn("data-pill-remove", html)
|
self.assertNotIn("data-pill-remove", html)
|
||||||
|
|
||||||
def test_removable_adds_remove_button(self):
|
def test_removable_adds_remove_button(self):
|
||||||
html = Pill("hi", removable=True)
|
html = str(Pill("hi", removable=True))
|
||||||
self.assertIn("data-pill-remove", html)
|
self.assertIn("data-pill-remove", html)
|
||||||
self.assertIn('aria-label="Remove"', html)
|
self.assertIn('aria-label="Remove"', html)
|
||||||
|
|
||||||
def test_value_becomes_data_value(self):
|
def test_value_becomes_data_value(self):
|
||||||
html = Pill("hi", value="42")
|
html = str(Pill("hi", value="42"))
|
||||||
self.assertIn('data-value="42"', html)
|
self.assertIn('data-value="42"', html)
|
||||||
|
|
||||||
def test_no_value_omits_data_value(self):
|
def test_no_value_omits_data_value(self):
|
||||||
self.assertNotIn("data-value", Pill("hi"))
|
self.assertNotIn("data-value", str(Pill("hi")))
|
||||||
|
|
||||||
def test_label_is_escaped(self):
|
def test_label_is_escaped(self):
|
||||||
html = Pill("<b>x</b>")
|
html = str(Pill("<b>x</b>"))
|
||||||
self.assertIn("<b>", html)
|
self.assertIn("<b>", html)
|
||||||
self.assertNotIn("<b>x</b>", html)
|
self.assertNotIn("<b>x</b>", html)
|
||||||
|
|
||||||
def test_extra_data_attributes(self):
|
def test_extra_data_attributes(self):
|
||||||
html = Pill("hi", attributes=[("data-platform", "3")])
|
html = str(Pill("hi", attributes=[("data-platform", "3")]))
|
||||||
self.assertIn('data-platform="3"', html)
|
self.assertIn('data-platform="3"', html)
|
||||||
|
|
||||||
|
|
||||||
class SearchSelectComponentTest(unittest.TestCase):
|
class SearchSelectComponentTest(unittest.TestCase):
|
||||||
def test_returns_safetext(self):
|
def test_returns_safetext(self):
|
||||||
self.assertIsInstance(SearchSelect(name="games"), SafeText)
|
self.assertIsInstance(str(SearchSelect(name="games")), SafeText)
|
||||||
|
|
||||||
def test_empty_options_renders_no_results_scaffold(self):
|
def test_empty_options_renders_no_results_scaffold(self):
|
||||||
html = SearchSelect(name="games")
|
html = str(SearchSelect(name="games"))
|
||||||
self.assertIn("data-search-select-no-results", html)
|
self.assertIn("data-search-select-no-results", html)
|
||||||
self.assertIn("No results", html)
|
self.assertIn("No results", html)
|
||||||
|
|
||||||
def test_outer_container_carries_config(self):
|
def test_outer_container_carries_config(self):
|
||||||
html = SearchSelect(
|
html = str(
|
||||||
|
SearchSelect(
|
||||||
name="games", search_url="/api/games/search", multi_select=True
|
name="games", search_url="/api/games/search", multi_select=True
|
||||||
)
|
)
|
||||||
|
)
|
||||||
self.assertIn("data-search-select", html)
|
self.assertIn("data-search-select", html)
|
||||||
self.assertIn('data-name="games"', html)
|
self.assertIn('data-name="games"', html)
|
||||||
self.assertIn('data-search-url="/api/games/search"', html)
|
self.assertIn('data-search-url="/api/games/search"', html)
|
||||||
self.assertIn('data-multi="true"', html)
|
self.assertIn('data-multi="true"', html)
|
||||||
|
|
||||||
def test_multi_selected_renders_pills_and_hidden_inputs(self):
|
def test_multi_selected_renders_pills_and_hidden_inputs(self):
|
||||||
html = SearchSelect(
|
html = str(
|
||||||
|
SearchSelect(
|
||||||
name="games",
|
name="games",
|
||||||
multi_select=True,
|
multi_select=True,
|
||||||
selected=[{"value": 7, "label": "Game A", "data": {"platform": "2"}}],
|
selected=[{"value": 7, "label": "Game A", "data": {"platform": "2"}}],
|
||||||
)
|
)
|
||||||
|
)
|
||||||
self.assertIn("data-pill", html)
|
self.assertIn("data-pill", html)
|
||||||
self.assertIn('<input name="games" value="7" type="hidden">', html)
|
self.assertIn('<input name="games" value="7" type="hidden">', html)
|
||||||
self.assertIn('data-platform="2"', html)
|
self.assertIn('data-platform="2"', html)
|
||||||
@@ -94,10 +85,12 @@ class SearchSelectComponentTest(unittest.TestCase):
|
|||||||
self.assertEqual(html.count(' name="games"'), 1)
|
self.assertEqual(html.count(' name="games"'), 1)
|
||||||
|
|
||||||
def test_single_selected_has_no_pill_and_value_in_search_box(self):
|
def test_single_selected_has_no_pill_and_value_in_search_box(self):
|
||||||
html = SearchSelect(
|
html = str(
|
||||||
|
SearchSelect(
|
||||||
name="games",
|
name="games",
|
||||||
selected=[{"value": 7, "label": "Game A", "data": {"platform": "2"}}],
|
selected=[{"value": 7, "label": "Game A", "data": {"platform": "2"}}],
|
||||||
)
|
)
|
||||||
|
)
|
||||||
# single-select renders no pill — the label lives in the search box
|
# single-select renders no pill — the label lives in the search box
|
||||||
self.assertNotIn("data-pill", html)
|
self.assertNotIn("data-pill", html)
|
||||||
self.assertIn('value="Game A"', html)
|
self.assertIn('value="Game A"', html)
|
||||||
@@ -106,21 +99,23 @@ class SearchSelectComponentTest(unittest.TestCase):
|
|||||||
self.assertEqual(html.count(' name="games"'), 1)
|
self.assertEqual(html.count(' name="games"'), 1)
|
||||||
|
|
||||||
def test_search_box_has_no_name(self):
|
def test_search_box_has_no_name(self):
|
||||||
html = SearchSelect(name="games")
|
html = str(SearchSelect(name="games"))
|
||||||
self.assertIn("data-search-select-search", html)
|
self.assertIn("data-search-select-search", html)
|
||||||
# container exposes data-name, never a submittable name on the search box
|
# container exposes data-name, never a submittable name on the search box
|
||||||
self.assertEqual(html.count(' name="games"'), 0)
|
self.assertEqual(html.count(' name="games"'), 0)
|
||||||
|
|
||||||
def test_tuple_options_are_normalized(self):
|
def test_tuple_options_are_normalized(self):
|
||||||
html = SearchSelect(name="t", options=[("1", "One")])
|
html = str(SearchSelect(name="t", options=[("1", "One")]))
|
||||||
self.assertIn('data-search-select-option=""', html)
|
self.assertIn('data-search-select-option=""', html)
|
||||||
self.assertIn('data-value="1"', html)
|
self.assertIn('data-value="1"', html)
|
||||||
self.assertIn("One", html)
|
self.assertIn("One", html)
|
||||||
|
|
||||||
def test_options_omitted_when_search_url_set(self):
|
def test_options_omitted_when_search_url_set(self):
|
||||||
html = SearchSelect(
|
html = str(
|
||||||
|
SearchSelect(
|
||||||
name="t", options=[("1", "One")], search_url="/api/games/search"
|
name="t", options=[("1", "One")], search_url="/api/games/search"
|
||||||
)
|
)
|
||||||
|
)
|
||||||
# No pre-rendered rows in the live panel; the row prototype lives only in
|
# No pre-rendered rows in the live panel; the row prototype lives only in
|
||||||
# the cloneable <template>.
|
# the cloneable <template>.
|
||||||
panel = html.split("data-search-select-template")[0]
|
panel = html.split("data-search-select-template")[0]
|
||||||
@@ -130,7 +125,9 @@ class SearchSelectComponentTest(unittest.TestCase):
|
|||||||
def test_templates_carry_label_slot_for_js_cloning(self):
|
def test_templates_carry_label_slot_for_js_cloning(self):
|
||||||
# The dynamic shapes the JS clones expose a [data-search-select-label] slot so the JS
|
# The dynamic shapes the JS clones expose a [data-search-select-label] slot so the JS
|
||||||
# only fills text — classes/structure stay server-side.
|
# only fills text — classes/structure stay server-side.
|
||||||
html = SearchSelect(name="t", search_url="/api/games/search", multi_select=True)
|
html = str(
|
||||||
|
SearchSelect(name="t", search_url="/api/games/search", multi_select=True)
|
||||||
|
)
|
||||||
self.assertIn('data-search-select-template="row"', html)
|
self.assertIn('data-search-select-template="row"', html)
|
||||||
self.assertIn('data-search-select-template="pill"', html)
|
self.assertIn('data-search-select-template="pill"', html)
|
||||||
self.assertIn("data-search-select-label", html)
|
self.assertIn("data-search-select-label", html)
|
||||||
@@ -138,7 +135,7 @@ class SearchSelectComponentTest(unittest.TestCase):
|
|||||||
def test_shell_region_order_pills_search_options(self):
|
def test_shell_region_order_pills_search_options(self):
|
||||||
# The shared shell assembles the three regions in a fixed order; option
|
# The shared shell assembles the three regions in a fixed order; option
|
||||||
# rows precede the trailing no-results node inside the options panel.
|
# rows precede the trailing no-results node inside the options panel.
|
||||||
html = SearchSelect(name="t", options=[("1", "One")])
|
html = str(SearchSelect(name="t", options=[("1", "One")]))
|
||||||
pills = html.index("data-search-select-pills")
|
pills = html.index("data-search-select-pills")
|
||||||
search = html.index("data-search-select-search")
|
search = html.index("data-search-select-search")
|
||||||
options = html.index("data-search-select-options")
|
options = html.index("data-search-select-options")
|
||||||
@@ -151,11 +148,11 @@ class SearchSelectComponentTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_prefetch_attribute_and_defaults(self):
|
def test_prefetch_attribute_and_defaults(self):
|
||||||
# Default prefetch is 0 in SearchSelect
|
# Default prefetch is 0 in SearchSelect
|
||||||
html_default = SearchSelect(name="t")
|
html_default = str(SearchSelect(name="t"))
|
||||||
self.assertIn('data-prefetch="0"', html_default)
|
self.assertIn('data-prefetch="0"', html_default)
|
||||||
|
|
||||||
# Custom prefetch is rendered
|
# Custom prefetch is rendered
|
||||||
html_custom = SearchSelect(name="t", prefetch=42)
|
html_custom = str(SearchSelect(name="t", prefetch=42))
|
||||||
self.assertIn('data-prefetch="42"', html_custom)
|
self.assertIn('data-prefetch="42"', html_custom)
|
||||||
|
|
||||||
|
|
||||||
@@ -163,10 +160,10 @@ class FilterSelectComponentTest(unittest.TestCase):
|
|||||||
MODIFIERS = [("NOT_NULL", "(Any)"), ("IS_NULL", "(None)")]
|
MODIFIERS = [("NOT_NULL", "(Any)"), ("IS_NULL", "(None)")]
|
||||||
|
|
||||||
def test_returns_safetext(self):
|
def test_returns_safetext(self):
|
||||||
self.assertIsInstance(FilterSelect(field_name="type"), SafeText)
|
self.assertIsInstance(str(FilterSelect(field_name="type")), SafeText)
|
||||||
|
|
||||||
def test_is_filter_mode_on_shared_shell(self):
|
def test_is_filter_mode_on_shared_shell(self):
|
||||||
html = FilterSelect(field_name="type")
|
html = str(FilterSelect(field_name="type"))
|
||||||
# Reuses the SearchSelect shell (data-search-select) but flags filter mode.
|
# Reuses the SearchSelect shell (data-search-select) but flags filter mode.
|
||||||
self.assertIn("data-search-select", html)
|
self.assertIn("data-search-select", html)
|
||||||
self.assertIn('data-search-select-mode="filter"', html)
|
self.assertIn('data-search-select-mode="filter"', html)
|
||||||
@@ -175,18 +172,20 @@ class FilterSelectComponentTest(unittest.TestCase):
|
|||||||
self.assertEqual(html.count(' name="type"'), 0)
|
self.assertEqual(html.count(' name="type"'), 0)
|
||||||
|
|
||||||
def test_value_rows_have_include_exclude_buttons(self):
|
def test_value_rows_have_include_exclude_buttons(self):
|
||||||
html = FilterSelect(field_name="type", options=[("g", "Game")])
|
html = str(FilterSelect(field_name="type", options=[("g", "Game")]))
|
||||||
self.assertIn('data-search-select-action="include"', html)
|
self.assertIn('data-search-select-action="include"', html)
|
||||||
self.assertIn('data-search-select-action="exclude"', html)
|
self.assertIn('data-search-select-action="exclude"', html)
|
||||||
self.assertIn('data-value="g"', html)
|
self.assertIn('data-value="g"', html)
|
||||||
|
|
||||||
def test_included_renders_check_pill_excluded_renders_cross_pill(self):
|
def test_included_renders_check_pill_excluded_renders_cross_pill(self):
|
||||||
html = FilterSelect(
|
html = str(
|
||||||
|
FilterSelect(
|
||||||
field_name="platform",
|
field_name="platform",
|
||||||
options=[("1", "Steam"), ("2", "GOG")],
|
options=[("1", "Steam"), ("2", "GOG")],
|
||||||
included=[("1", "Steam")],
|
included=[("1", "Steam")],
|
||||||
excluded=[("2", "GOG")],
|
excluded=[("2", "GOG")],
|
||||||
)
|
)
|
||||||
|
)
|
||||||
# Labels live in a [data-search-select-label] slot (so JS can fill clones); the ✓/✗
|
# Labels live in a [data-search-select-label] slot (so JS can fill clones); the ✓/✗
|
||||||
# symbol is a sibling text node.
|
# symbol is a sibling text node.
|
||||||
self.assertIn('data-search-select-type="include"', html)
|
self.assertIn('data-search-select-type="include"', html)
|
||||||
@@ -198,7 +197,7 @@ class FilterSelectComponentTest(unittest.TestCase):
|
|||||||
self.assertIn("line-through", html) # excluded pill styling
|
self.assertIn("line-through", html) # excluded pill styling
|
||||||
|
|
||||||
def test_modifier_options_render_pinned_rows(self):
|
def test_modifier_options_render_pinned_rows(self):
|
||||||
html = FilterSelect(field_name="platform", modifier_options=self.MODIFIERS)
|
html = str(FilterSelect(field_name="platform", modifier_options=self.MODIFIERS))
|
||||||
# Pinned pseudo-options carry data-search-select-modifier-option, never data-search-select-option,
|
# Pinned pseudo-options carry data-search-select-modifier-option, never data-search-select-option,
|
||||||
# so the text filter leaves them visible.
|
# so the text filter leaves them visible.
|
||||||
self.assertIn('data-search-select-modifier-option="NOT_NULL"', html)
|
self.assertIn('data-search-select-modifier-option="NOT_NULL"', html)
|
||||||
@@ -207,13 +206,15 @@ class FilterSelectComponentTest(unittest.TestCase):
|
|||||||
def test_modifier_pill_coexists_with_value_pills(self):
|
def test_modifier_pill_coexists_with_value_pills(self):
|
||||||
"""Modifier and value pills both render server-side; the JS handles
|
"""Modifier and value pills both render server-side; the JS handles
|
||||||
mutual exclusivity for presence modifiers (PRESENCE_MODIFIERS)."""
|
mutual exclusivity for presence modifiers (PRESENCE_MODIFIERS)."""
|
||||||
html = FilterSelect(
|
html = str(
|
||||||
|
FilterSelect(
|
||||||
field_name="platform",
|
field_name="platform",
|
||||||
options=[("1", "Steam")],
|
options=[("1", "Steam")],
|
||||||
included=[("1", "Steam")],
|
included=[("1", "Steam")],
|
||||||
modifier="IS_NULL",
|
modifier="IS_NULL",
|
||||||
modifier_options=self.MODIFIERS,
|
modifier_options=self.MODIFIERS,
|
||||||
)
|
)
|
||||||
|
)
|
||||||
# Both the modifier pill and the value pill render.
|
# Both the modifier pill and the value pill render.
|
||||||
self.assertIn('data-search-select-modifier="IS_NULL"', html)
|
self.assertIn('data-search-select-modifier="IS_NULL"', html)
|
||||||
self.assertIn("(None)", html)
|
self.assertIn("(None)", html)
|
||||||
@@ -221,12 +222,14 @@ class FilterSelectComponentTest(unittest.TestCase):
|
|||||||
self.assertIn('data-modifier="IS_NULL"', html) # container carries it too
|
self.assertIn('data-modifier="IS_NULL"', html) # container carries it too
|
||||||
|
|
||||||
def test_search_url_omits_value_rows_but_keeps_modifiers(self):
|
def test_search_url_omits_value_rows_but_keeps_modifiers(self):
|
||||||
html = FilterSelect(
|
html = str(
|
||||||
|
FilterSelect(
|
||||||
field_name="game",
|
field_name="game",
|
||||||
search_url="/api/games/search",
|
search_url="/api/games/search",
|
||||||
prefetch=20,
|
prefetch=20,
|
||||||
modifier_options=self.MODIFIERS,
|
modifier_options=self.MODIFIERS,
|
||||||
)
|
)
|
||||||
|
)
|
||||||
# No value rows in the live panel (they're fetched); the row prototype
|
# No value rows in the live panel (they're fetched); the row prototype
|
||||||
# lives only in a <template>.
|
# lives only in a <template>.
|
||||||
panel = html.split("data-search-select-template")[0]
|
panel = html.split("data-search-select-template")[0]
|
||||||
@@ -239,11 +242,13 @@ class FilterSelectComponentTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_search_url_pills_use_resolved_labels(self):
|
def test_search_url_pills_use_resolved_labels(self):
|
||||||
# A selected value outside the fetched window still shows its label.
|
# A selected value outside the fetched window still shows its label.
|
||||||
html = FilterSelect(
|
html = str(
|
||||||
|
FilterSelect(
|
||||||
field_name="game",
|
field_name="game",
|
||||||
search_url="/api/games/search",
|
search_url="/api/games/search",
|
||||||
excluded=[{"value": 4172, "label": "Obscure Game", "data": {}}],
|
excluded=[{"value": 4172, "label": "Obscure Game", "data": {}}],
|
||||||
)
|
)
|
||||||
|
)
|
||||||
self.assertIn(">Obscure Game</span>", html)
|
self.assertIn(">Obscure Game</span>", html)
|
||||||
self.assertIn('data-value="4172"', html)
|
self.assertIn('data-value="4172"', html)
|
||||||
|
|
||||||
@@ -255,7 +260,8 @@ class FilterSelectComponentTest(unittest.TestCase):
|
|||||||
def test_m2m_modifiers_render_as_option_rows(self):
|
def test_m2m_modifiers_render_as_option_rows(self):
|
||||||
"""M2M modifiers (All)/(Only) render as modifier-option rows in the
|
"""M2M modifiers (All)/(Only) render as modifier-option rows in the
|
||||||
dropdown, not as a separate <select>."""
|
dropdown, not as a separate <select>."""
|
||||||
html = FilterSelect(
|
html = str(
|
||||||
|
FilterSelect(
|
||||||
field_name="games",
|
field_name="games",
|
||||||
modifier_options=[
|
modifier_options=[
|
||||||
("NOT_NULL", "(Any)"),
|
("NOT_NULL", "(Any)"),
|
||||||
@@ -264,6 +270,7 @@ class FilterSelectComponentTest(unittest.TestCase):
|
|||||||
("INCLUDES_ONLY", "(Only)"),
|
("INCLUDES_ONLY", "(Only)"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
)
|
||||||
self.assertIn('data-search-select-modifier-option="INCLUDES_ALL"', 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="INCLUDES_ONLY"', html)
|
||||||
self.assertIn('data-search-select-modifier-option="NOT_NULL"', html)
|
self.assertIn('data-search-select-modifier-option="NOT_NULL"', html)
|
||||||
@@ -273,7 +280,8 @@ class FilterSelectComponentTest(unittest.TestCase):
|
|||||||
def test_active_modifier_renders_pill(self):
|
def test_active_modifier_renders_pill(self):
|
||||||
"""When modifier is INCLUDES_ALL, the modifier pill renders with the
|
"""When modifier is INCLUDES_ALL, the modifier pill renders with the
|
||||||
(All) label alongside any value pills."""
|
(All) label alongside any value pills."""
|
||||||
html = FilterSelect(
|
html = str(
|
||||||
|
FilterSelect(
|
||||||
field_name="games",
|
field_name="games",
|
||||||
modifier="INCLUDES_ALL",
|
modifier="INCLUDES_ALL",
|
||||||
modifier_options=[
|
modifier_options=[
|
||||||
@@ -284,6 +292,7 @@ class FilterSelectComponentTest(unittest.TestCase):
|
|||||||
],
|
],
|
||||||
included=[{"value": 5, "label": "Hollow Knight", "data": {}}],
|
included=[{"value": 5, "label": "Hollow Knight", "data": {}}],
|
||||||
)
|
)
|
||||||
|
)
|
||||||
self.assertIn('data-modifier="INCLUDES_ALL"', html)
|
self.assertIn('data-modifier="INCLUDES_ALL"', html)
|
||||||
self.assertIn("(All)", html)
|
self.assertIn("(All)", html)
|
||||||
self.assertIn("Hollow Knight", html)
|
self.assertIn("Hollow Knight", html)
|
||||||
@@ -291,11 +300,13 @@ class FilterSelectComponentTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_presence_only_modifiers_no_m2m_rows(self):
|
def test_presence_only_modifiers_no_m2m_rows(self):
|
||||||
"""When modifier_options only has presence entries, no M2M rows appear."""
|
"""When modifier_options only has presence entries, no M2M rows appear."""
|
||||||
html = FilterSelect(
|
html = str(
|
||||||
|
FilterSelect(
|
||||||
field_name="status",
|
field_name="status",
|
||||||
modifier_options=[("NOT_NULL", "(Any)"), ("IS_NULL", "(None)")],
|
modifier_options=[("NOT_NULL", "(Any)"), ("IS_NULL", "(None)")],
|
||||||
options=[("f", "Finished")],
|
options=[("f", "Finished")],
|
||||||
)
|
)
|
||||||
|
)
|
||||||
self.assertNotIn("INCLUDES_ALL", html)
|
self.assertNotIn("INCLUDES_ALL", html)
|
||||||
self.assertNotIn("INCLUDES_ONLY", html)
|
self.assertNotIn("INCLUDES_ONLY", html)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user