"""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 ( FilterSelect, Pill, SearchSelect, searchselect_selected, ) from games.models import Game, Platform class PillTest(unittest.TestCase): def test_returns_safetext(self): self.assertIsInstance(Pill("hi"), SafeText) def test_plain_pill_has_data_pill_no_remove(self): html = Pill("hi") self.assertIn("data-pill", html) self.assertNotIn("data-pill-remove", html) def test_removable_adds_remove_button(self): html = Pill("hi", removable=True) self.assertIn("data-pill-remove", html) self.assertIn('aria-label="Remove"', html) def test_value_becomes_data_value(self): html = Pill("hi", value="42") self.assertIn('data-value="42"', html) def test_no_value_omits_data_value(self): self.assertNotIn("data-value", Pill("hi")) def test_label_is_escaped(self): html = Pill("x") self.assertIn("<b>", html) self.assertNotIn("x", html) def test_extra_data_attributes(self): html = Pill("hi", attributes=[("data-platform", "3")]) self.assertIn('data-platform="3"', html) class SearchSelectComponentTest(unittest.TestCase): def test_returns_safetext(self): self.assertIsInstance(SearchSelect(name="games"), SafeText) def test_empty_options_renders_no_results_scaffold(self): html = SearchSelect(name="games") self.assertIn("data-search-select-no-results", html) self.assertIn("No results", html) def test_outer_container_carries_config(self): html = 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 = 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 = 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 = 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 = 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 = 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