Expand the ss namespace prefix to search-select everywhere

Spell out the abbreviated data-ss-* hook attributes (data-search-select-option,
-label, -mode, -template, -action, -type, -modifier, -modifier-option, -pills,
-search, -options, -no-results) and the JS expando properties (_searchSelectInit,
_searchSelectLabel, _searchSelectDirty, _searchSelectOption) across components,
JS, and tests — no abbreviations left in the widget's hooks.

https://claude.ai/code/session_01XzhXvMvw42CQGc9kmin3GS
This commit is contained in:
Claude
2026-06-08 14:13:05 +00:00
committed by Lukáš Kucharczyk
parent a06e772e42
commit 15bb3ce1b9
7 changed files with 106 additions and 97 deletions
+32 -32
View File
@@ -52,7 +52,7 @@ class SearchSelectComponentTest(unittest.TestCase):
def test_empty_options_renders_no_results_scaffold(self):
html = SearchSelect(name="games")
self.assertIn("data-ss-no-results", html)
self.assertIn("data-search-select-no-results", html)
self.assertIn("No results", html)
def test_outer_container_carries_config(self):
@@ -91,13 +91,13 @@ class SearchSelectComponentTest(unittest.TestCase):
def test_search_box_has_no_name(self):
html = SearchSelect(name="games")
self.assertIn("data-ss-search", html)
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-ss-option=""', html)
self.assertIn('data-search-select-option=""', html)
self.assertIn('data-value="1"', html)
self.assertIn("One", html)
@@ -107,27 +107,27 @@ class SearchSelectComponentTest(unittest.TestCase):
)
# No pre-rendered rows in the live panel; the row prototype lives only in
# the cloneable <template>.
panel = html.split("data-ss-template")[0]
self.assertNotIn('data-ss-option=""', panel)
self.assertIn('data-ss-template="row"', html)
panel = html.split("data-search-select-template")[0]
self.assertNotIn('data-search-select-option=""', panel)
self.assertIn('data-search-select-template="row"', html)
def test_templates_carry_label_slot_for_js_cloning(self):
# The dynamic shapes the JS clones expose a [data-ss-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.
html = SearchSelect(name="t", search_url="/api/games/search", multi_select=True)
self.assertIn('data-ss-template="row"', html)
self.assertIn('data-ss-template="pill"', html)
self.assertIn("data-ss-label", html)
self.assertIn('data-search-select-template="row"', html)
self.assertIn('data-search-select-template="pill"', html)
self.assertIn("data-search-select-label", html)
def test_shell_region_order_pills_search_options(self):
# The shared shell assembles the three regions in a fixed order; option
# rows precede the trailing no-results node inside the options panel.
html = SearchSelect(name="t", options=[("1", "One")])
pills = html.index("data-ss-pills")
search = html.index("data-ss-search")
options = html.index("data-ss-options")
option_row = html.index('data-ss-option=""')
no_results = html.index("data-ss-no-results")
pills = html.index("data-search-select-pills")
search = html.index("data-search-select-search")
options = html.index("data-search-select-options")
option_row = html.index('data-search-select-option=""')
no_results = html.index("data-search-select-no-results")
self.assertLess(pills, search)
self.assertLess(search, options)
self.assertLess(options, option_row)
@@ -144,15 +144,15 @@ class FilterSelectComponentTest(unittest.TestCase):
html = FilterSelect(field_name="type")
# Reuses the SearchSelect shell (data-search-select) but flags filter mode.
self.assertIn("data-search-select", html)
self.assertIn('data-ss-mode="filter"', html)
self.assertIn('data-search-select-mode="filter"', html)
self.assertIn('data-name="type"', html)
# No name is submitted — state is read from the DOM into the filter JSON.
self.assertEqual(html.count(' name="type"'), 0)
def test_value_rows_have_include_exclude_buttons(self):
html = FilterSelect(field_name="type", options=[("g", "Game")])
self.assertIn('data-ss-action="include"', html)
self.assertIn('data-ss-action="exclude"', html)
self.assertIn('data-search-select-action="include"', html)
self.assertIn('data-search-select-action="exclude"', html)
self.assertIn('data-value="g"', html)
def test_included_renders_check_pill_excluded_renders_cross_pill(self):
@@ -162,22 +162,22 @@ class FilterSelectComponentTest(unittest.TestCase):
included=[("1", "Steam")],
excluded=[("2", "GOG")],
)
# Labels live in a [data-ss-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.
self.assertIn('data-ss-type="include"', html)
self.assertIn('data-search-select-type="include"', html)
self.assertIn("", html)
self.assertIn(">Steam</span>", html)
self.assertIn('data-ss-type="exclude"', html)
self.assertIn('data-search-select-type="exclude"', html)
self.assertIn("", html)
self.assertIn(">GOG</span>", html)
self.assertIn("line-through", html) # excluded pill styling
def test_modifier_options_render_pinned_rows(self):
html = FilterSelect(field_name="platform", modifier_options=self.MODIFIERS)
# Pinned pseudo-options carry data-ss-modifier-option, never data-ss-option,
# Pinned pseudo-options carry data-search-select-modifier-option, never data-search-select-option,
# so the text filter leaves them visible.
self.assertIn('data-ss-modifier-option="NOT_NULL"', html)
self.assertIn('data-ss-modifier-option="IS_NULL"', html)
self.assertIn('data-search-select-modifier-option="NOT_NULL"', html)
self.assertIn('data-search-select-modifier-option="IS_NULL"', html)
def test_active_modifier_replaces_value_pills(self):
html = FilterSelect(
@@ -189,11 +189,11 @@ class FilterSelectComponentTest(unittest.TestCase):
)
# The lone modifier pill is shown; include/exclude pills are suppressed.
# (Scope the check to the live pills region — the cloneable pill <template>s
# legitimately contain data-ss-type.)
pills_region = html.split("data-ss-template")[0]
self.assertIn('data-ss-modifier="IS_NULL"', html)
# legitimately contain data-search-select-type.)
pills_region = html.split("data-search-select-template")[0]
self.assertIn('data-search-select-modifier="IS_NULL"', html)
self.assertIn("(None)", html)
self.assertNotIn('data-ss-type="include"', pills_region)
self.assertNotIn('data-search-select-type="include"', pills_region)
self.assertIn('data-modifier="IS_NULL"', html) # container carries it too
def test_search_url_omits_value_rows_but_keeps_modifiers(self):
@@ -205,10 +205,10 @@ class FilterSelectComponentTest(unittest.TestCase):
)
# No value rows in the live panel (they're fetched); the row prototype
# lives only in a <template>.
panel = html.split("data-ss-template")[0]
self.assertNotIn('data-ss-option=""', panel)
self.assertIn('data-ss-template="row"', html)
self.assertIn('data-ss-modifier-option="NOT_NULL"', html) # still pinned
panel = html.split("data-search-select-template")[0]
self.assertNotIn('data-search-select-option=""', panel)
self.assertIn('data-search-select-template="row"', html)
self.assertIn('data-search-select-modifier-option="NOT_NULL"', html) # still pinned
self.assertIn('data-prefetch="20"', html)
def test_search_url_pills_use_resolved_labels(self):