"""Tests for the SearchSelect component, the Pill primitive, the games resolver, the search API endpoint, and the shared Game.search_label.""" import unittest import django.test from django.utils.safestring import SafeText from common.components import ( searchselect_selected, ) from common.components import FilterSelect, Pill, SearchSelect from games.models import Game, Platform # These components are lazy nodes; the tests below assert on rendered HTML, so # each call is wrapped in ``str(...)`` (``Node.__str__`` returns a ``SafeText``, # which keeps the ``assertIsInstance(..., SafeText)`` checks meaningful and the # string assertions working). class PillTest(unittest.TestCase): def test_returns_safetext(self): self.assertIsInstance(str(Pill("hi")), SafeText) def test_plain_pill_has_data_pill_no_remove(self): html = str(Pill("hi")) self.assertIn("data-pill", html) self.assertNotIn("data-pill-remove", html) def test_removable_adds_remove_button(self): html = str(Pill("hi", removable=True)) self.assertIn("data-pill-remove", html) self.assertIn('aria-label="Remove"', html) def test_value_becomes_data_value(self): html = str(Pill("hi", value="42")) self.assertIn('data-value="42"', html) def test_no_value_omits_data_value(self): self.assertNotIn("data-value", str(Pill("hi"))) def test_label_is_escaped(self): html = str(Pill("x")) self.assertIn("<b>", html) self.assertNotIn("x", html) def test_extra_data_attributes(self): html = str(Pill("hi", attributes=[("data-platform", "3")])) self.assertIn('data-platform="3"', html) class SearchSelectComponentTest(unittest.TestCase): def test_returns_safetext(self): self.assertIsInstance(str(SearchSelect(name="games")), SafeText) def test_empty_options_renders_no_results_scaffold(self): html = str(SearchSelect(name="games")) self.assertIn("data-search-select-no-results", html) self.assertIn("No results", html) def test_outer_container_carries_config(self): html = str( SearchSelect( name="games", search_url="/api/games/search", multi_select=True ) ) self.assertIn("data-search-select", html) self.assertIn('data-name="games"', html) self.assertIn('data-search-url="/api/games/search"', html) self.assertIn('data-multi="true"', html) def test_multi_selected_renders_pills_and_hidden_inputs(self): html = str( SearchSelect( name="games", multi_select=True, selected=[{"value": 7, "label": "Game A", "data": {"platform": "2"}}], ) ) self.assertIn("data-pill", html) self.assertIn('', html) self.assertIn('data-platform="2"', html) # exactly one submitted value (the hidden input) — the search box has no # name. The leading space avoids matching the container's data-name. self.assertEqual(html.count(' name="games"'), 1) def test_single_selected_has_no_pill_and_value_in_search_box(self): html = str( SearchSelect( name="games", selected=[{"value": 7, "label": "Game A", "data": {"platform": "2"}}], ) ) # single-select renders no pill — the label lives in the search box self.assertNotIn("data-pill", html) self.assertIn('value="Game A"', html) # the value is still submitted via a lone hidden input self.assertIn('', html) self.assertEqual(html.count(' name="games"'), 1) def test_search_box_has_no_name(self): html = str(SearchSelect(name="games")) self.assertIn("data-search-select-search", html) # container exposes data-name, never a submittable name on the search box self.assertEqual(html.count(' name="games"'), 0) def test_tuple_options_are_normalized(self): html = str(SearchSelect(name="t", options=[("1", "One")])) self.assertIn('data-search-select-option=""', html) self.assertIn('data-value="1"', html) self.assertIn("One", html) def test_options_omitted_when_search_url_set(self): html = str( SearchSelect( name="t", options=[("1", "One")], search_url="/api/games/search" ) ) # No pre-rendered rows in the live panel; the row prototype lives only in # the cloneable