All checks were successful
Deploy / deploy (push) Successful in 1m50s
- Create reusable dsl_facets.py component with clickable pills that compose DSL filter expressions by appending field:value to the filter input - Add /api/facets endpoint for dynamic facet count refresh via HTMX - Fix select dropdown dark mode styling with color-scheme: dark in SelectStyles - Integrate facet pills into all DSL filter screens: registry, move, and all action forms (tag-add, tag-end, attrs, outcome, status-correct) - Update routes to fetch and pass facet counts, locations, and species - Add comprehensive unit tests for component and API endpoint - Add E2E tests for facet pill click behavior and dark mode select visibility This enables tap-based filter composition on mobile without requiring typing. Facet counts update dynamically as filters are applied via HTMX. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
234 lines
7.9 KiB
Python
234 lines
7.9 KiB
Python
# ABOUTME: Unit tests for DSL facet pills template component.
|
|
# ABOUTME: Tests HTML generation for facet pill structure and HTMX attributes.
|
|
|
|
from fasthtml.common import to_xml
|
|
|
|
from animaltrack.repositories.animals import FacetCounts
|
|
|
|
|
|
class TestDslFacetPills:
|
|
"""Test the dsl_facet_pills component."""
|
|
|
|
def test_facet_pills_renders_with_counts(self):
|
|
"""Facet pills component renders species counts as pills."""
|
|
from animaltrack.web.templates.dsl_facets import dsl_facet_pills
|
|
|
|
facets = FacetCounts(
|
|
by_species={"duck": 5, "goose": 3},
|
|
by_sex={"female": 4, "male": 3, "unknown": 1},
|
|
by_life_stage={"adult": 6, "juvenile": 2},
|
|
by_location={"loc1": 5, "loc2": 3},
|
|
)
|
|
locations = []
|
|
species_list = []
|
|
|
|
result = dsl_facet_pills(facets, "filter", locations, species_list)
|
|
html = to_xml(result)
|
|
|
|
# Should have container with proper ID
|
|
assert 'id="dsl-facet-pills"' in html
|
|
# Should have data attributes for JavaScript
|
|
assert 'data-facet-field="species"' in html
|
|
assert 'data-facet-value="duck"' in html
|
|
assert 'data-facet-value="goose"' in html
|
|
|
|
def test_facet_pills_has_htmx_attributes_for_refresh(self):
|
|
"""Facet pills container has HTMX attributes for dynamic refresh."""
|
|
from animaltrack.web.templates.dsl_facets import dsl_facet_pills
|
|
|
|
facets = FacetCounts(
|
|
by_species={"duck": 5},
|
|
by_sex={},
|
|
by_life_stage={},
|
|
by_location={},
|
|
)
|
|
|
|
result = dsl_facet_pills(facets, "filter", [], [])
|
|
html = to_xml(result)
|
|
|
|
# Should have HTMX attributes for updating facets
|
|
assert "hx-get" in html
|
|
assert "/api/facets" in html
|
|
assert "hx-trigger" in html
|
|
assert "#filter" in html # References the filter input
|
|
|
|
def test_facet_pills_renders_all_facet_sections(self):
|
|
"""Facet pills renders species, sex, life_stage, and location sections."""
|
|
from animaltrack.web.templates.dsl_facets import dsl_facet_pills
|
|
|
|
facets = FacetCounts(
|
|
by_species={"duck": 5},
|
|
by_sex={"female": 3},
|
|
by_life_stage={"adult": 4},
|
|
by_location={"loc1": 5},
|
|
)
|
|
|
|
result = dsl_facet_pills(facets, "filter", [], [])
|
|
html = to_xml(result)
|
|
|
|
# Should have all section headers
|
|
assert "Species" in html
|
|
assert "Sex" in html
|
|
assert "Life Stage" in html
|
|
assert "Location" in html
|
|
|
|
def test_facet_pills_includes_counts_in_pills(self):
|
|
"""Each pill shows the count alongside the label."""
|
|
from animaltrack.web.templates.dsl_facets import dsl_facet_pills
|
|
|
|
facets = FacetCounts(
|
|
by_species={"duck": 12},
|
|
by_sex={},
|
|
by_life_stage={},
|
|
by_location={},
|
|
)
|
|
|
|
result = dsl_facet_pills(facets, "filter", [], [])
|
|
html = to_xml(result)
|
|
|
|
# Should show count 12
|
|
assert ">12<" in html or ">12 " in html or " 12<" in html
|
|
|
|
def test_facet_pills_uses_location_names(self):
|
|
"""Location facets use human-readable names from location list."""
|
|
from animaltrack.models.reference import Location
|
|
from animaltrack.web.templates.dsl_facets import dsl_facet_pills
|
|
|
|
facets = FacetCounts(
|
|
by_species={},
|
|
by_sex={},
|
|
by_life_stage={},
|
|
by_location={"01ARZ3NDEKTSV4RRFFQ69G5FAV": 5},
|
|
)
|
|
locations = [
|
|
Location(
|
|
id="01ARZ3NDEKTSV4RRFFQ69G5FAV",
|
|
name="Strip 1",
|
|
active=True,
|
|
created_at_utc=0,
|
|
updated_at_utc=0,
|
|
)
|
|
]
|
|
|
|
result = dsl_facet_pills(facets, "filter", locations, [])
|
|
html = to_xml(result)
|
|
|
|
# Should display location name
|
|
assert "Strip 1" in html
|
|
|
|
def test_facet_pills_uses_species_names(self):
|
|
"""Species facets use human-readable names from species list."""
|
|
from animaltrack.models.reference import Species
|
|
from animaltrack.web.templates.dsl_facets import dsl_facet_pills
|
|
|
|
facets = FacetCounts(
|
|
by_species={"duck": 5},
|
|
by_sex={},
|
|
by_life_stage={},
|
|
by_location={},
|
|
)
|
|
species_list = [
|
|
Species(
|
|
code="duck",
|
|
name="Duck",
|
|
active=True,
|
|
created_at_utc=0,
|
|
updated_at_utc=0,
|
|
)
|
|
]
|
|
|
|
result = dsl_facet_pills(facets, "filter", [], species_list)
|
|
html = to_xml(result)
|
|
|
|
# Should display species name
|
|
assert "Duck" in html
|
|
|
|
def test_facet_pills_empty_facets_not_shown(self):
|
|
"""Empty facet sections are not rendered."""
|
|
from animaltrack.web.templates.dsl_facets import dsl_facet_pills
|
|
|
|
facets = FacetCounts(
|
|
by_species={"duck": 5},
|
|
by_sex={}, # Empty
|
|
by_life_stage={}, # Empty
|
|
by_location={}, # Empty
|
|
)
|
|
|
|
result = dsl_facet_pills(facets, "filter", [], [])
|
|
html = to_xml(result)
|
|
|
|
# Should show Species but not empty sections
|
|
assert "Species" in html
|
|
# Sex section header should not appear since no sex facets
|
|
# (we count section headers, not raw word occurrences)
|
|
|
|
def test_facet_pills_onclick_calls_javascript(self):
|
|
"""Pill click handler uses JavaScript to update filter."""
|
|
from animaltrack.web.templates.dsl_facets import dsl_facet_pills
|
|
|
|
facets = FacetCounts(
|
|
by_species={"duck": 5},
|
|
by_sex={},
|
|
by_life_stage={},
|
|
by_location={},
|
|
)
|
|
|
|
result = dsl_facet_pills(facets, "filter", [], [])
|
|
html = to_xml(result)
|
|
|
|
# Should have onclick or similar handler
|
|
assert "onclick" in html or "hx-on:click" in html
|
|
|
|
|
|
class TestFacetPillsSection:
|
|
"""Test the facet_pill_section helper function."""
|
|
|
|
def test_section_sorts_by_count_descending(self):
|
|
"""Pills are sorted by count in descending order."""
|
|
from animaltrack.web.templates.dsl_facets import facet_pill_section
|
|
|
|
counts = {"a": 1, "b": 5, "c": 3}
|
|
result = facet_pill_section("Test", counts, "filter", "field")
|
|
html = to_xml(result)
|
|
|
|
# "b" (count 5) should appear before "c" (count 3) which appears before "a" (count 1)
|
|
pos_b = html.find('data-facet-value="b"')
|
|
pos_c = html.find('data-facet-value="c"')
|
|
pos_a = html.find('data-facet-value="a"')
|
|
|
|
assert pos_b < pos_c < pos_a, "Pills should be sorted by count descending"
|
|
|
|
def test_section_returns_none_for_empty_counts(self):
|
|
"""Empty counts returns None (no section rendered)."""
|
|
from animaltrack.web.templates.dsl_facets import facet_pill_section
|
|
|
|
result = facet_pill_section("Test", {}, "filter", "field")
|
|
assert result is None
|
|
|
|
def test_section_applies_label_map(self):
|
|
"""Label map transforms values to display labels."""
|
|
from animaltrack.web.templates.dsl_facets import facet_pill_section
|
|
|
|
counts = {"val1": 5}
|
|
label_map = {"val1": "Display Label"}
|
|
result = facet_pill_section("Test", counts, "filter", "field", label_map)
|
|
html = to_xml(result)
|
|
|
|
assert "Display Label" in html
|
|
|
|
|
|
class TestDslFacetPillsScript:
|
|
"""Test the JavaScript for facet pills interaction."""
|
|
|
|
def test_script_included_in_component(self):
|
|
"""Facet pills component includes the JavaScript for interaction."""
|
|
from animaltrack.web.templates.dsl_facets import dsl_facet_pills_script
|
|
|
|
result = dsl_facet_pills_script("filter")
|
|
html = to_xml(result)
|
|
|
|
# Should be a script element
|
|
assert "<script" in html.lower()
|
|
# Should have function to handle pill clicks
|
|
assert "appendFacetToFilter" in html or "addFacetToFilter" in html
|