Add clickable facet pills for mobile-friendly DSL filter composition
All checks were successful
Deploy / deploy (push) Successful in 1m50s
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>
This commit is contained in:
192
tests/e2e/test_facet_pills.py
Normal file
192
tests/e2e/test_facet_pills.py
Normal file
@@ -0,0 +1,192 @@
|
||||
# ABOUTME: E2E tests for DSL facet pills component.
|
||||
# ABOUTME: Tests click-to-filter, dynamic count updates, and dark mode visibility.
|
||||
|
||||
import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
|
||||
pytestmark = pytest.mark.e2e
|
||||
|
||||
|
||||
class TestFacetPillsOnMoveForm:
|
||||
"""Test facet pills functionality on the move form."""
|
||||
|
||||
def test_facet_pills_visible_on_move_page(self, page: Page, live_server):
|
||||
"""Verify facet pills section is visible on move page."""
|
||||
page.goto(f"{live_server.url}/move")
|
||||
expect(page.locator("body")).to_be_visible()
|
||||
|
||||
# Should see facet pills container
|
||||
facet_container = page.locator("#dsl-facet-pills")
|
||||
expect(facet_container).to_be_visible()
|
||||
|
||||
def test_click_species_facet_updates_filter(self, page: Page, live_server):
|
||||
"""Clicking a species facet pill updates the filter input."""
|
||||
page.goto(f"{live_server.url}/move")
|
||||
expect(page.locator("body")).to_be_visible()
|
||||
|
||||
# Click on a species facet pill (e.g., duck)
|
||||
duck_pill = page.locator('[data-facet-field="species"][data-facet-value="duck"]')
|
||||
expect(duck_pill).to_be_visible()
|
||||
duck_pill.click()
|
||||
|
||||
# Filter input should now contain species:duck
|
||||
filter_input = page.locator("#filter")
|
||||
expect(filter_input).to_have_value("species:duck")
|
||||
|
||||
def test_click_multiple_facets_composes_filter(self, page: Page, live_server):
|
||||
"""Clicking multiple facet pills composes the filter."""
|
||||
page.goto(f"{live_server.url}/move")
|
||||
expect(page.locator("body")).to_be_visible()
|
||||
|
||||
# Click species facet
|
||||
duck_pill = page.locator('[data-facet-field="species"][data-facet-value="duck"]')
|
||||
expect(duck_pill).to_be_visible()
|
||||
duck_pill.click()
|
||||
|
||||
# Click sex facet
|
||||
female_pill = page.locator('[data-facet-field="sex"][data-facet-value="female"]')
|
||||
expect(female_pill).to_be_visible()
|
||||
female_pill.click()
|
||||
|
||||
# Filter should contain both
|
||||
filter_input = page.locator("#filter")
|
||||
filter_value = filter_input.input_value()
|
||||
assert "species:duck" in filter_value
|
||||
assert "sex:female" in filter_value
|
||||
|
||||
def test_facet_counts_update_after_filter(self, page: Page, live_server):
|
||||
"""Facet counts update dynamically when filter changes."""
|
||||
page.goto(f"{live_server.url}/move")
|
||||
expect(page.locator("body")).to_be_visible()
|
||||
|
||||
# Get initial species counts
|
||||
facet_container = page.locator("#dsl-facet-pills")
|
||||
expect(facet_container).to_be_visible()
|
||||
|
||||
# Click species:duck to filter
|
||||
duck_pill = page.locator('[data-facet-field="species"][data-facet-value="duck"]')
|
||||
duck_pill.click()
|
||||
|
||||
# Wait for HTMX updates
|
||||
page.wait_for_timeout(1000)
|
||||
|
||||
# Facet counts should have updated - only alive duck-related counts shown
|
||||
# The sex facet should now show counts for ducks only
|
||||
sex_section = page.locator("#dsl-facet-pills").locator("text=Sex").locator("..")
|
||||
expect(sex_section).to_be_visible()
|
||||
|
||||
def test_selection_preview_updates_after_facet_click(self, page: Page, live_server):
|
||||
"""Selection preview updates after clicking a facet pill."""
|
||||
page.goto(f"{live_server.url}/move")
|
||||
expect(page.locator("body")).to_be_visible()
|
||||
|
||||
# Click species facet
|
||||
duck_pill = page.locator('[data-facet-field="species"][data-facet-value="duck"]')
|
||||
expect(duck_pill).to_be_visible()
|
||||
duck_pill.click()
|
||||
|
||||
# Wait for HTMX updates to selection container
|
||||
page.wait_for_timeout(1000)
|
||||
|
||||
# Selection container should show filtered animals
|
||||
selection_container = page.locator("#selection-container")
|
||||
expect(selection_container).to_be_visible()
|
||||
|
||||
|
||||
class TestFacetPillsOnOutcomeForm:
|
||||
"""Test facet pills functionality on the outcome form."""
|
||||
|
||||
def test_facet_pills_visible_on_outcome_page(self, page: Page, live_server):
|
||||
"""Verify facet pills section is visible on outcome page."""
|
||||
page.goto(f"{live_server.url}/actions/outcome")
|
||||
expect(page.locator("body")).to_be_visible()
|
||||
|
||||
# Should see facet pills container
|
||||
facet_container = page.locator("#dsl-facet-pills")
|
||||
expect(facet_container).to_be_visible()
|
||||
|
||||
def test_click_facet_on_outcome_form(self, page: Page, live_server):
|
||||
"""Clicking a facet pill on outcome form updates filter."""
|
||||
page.goto(f"{live_server.url}/actions/outcome")
|
||||
expect(page.locator("body")).to_be_visible()
|
||||
|
||||
# Click on a species facet pill
|
||||
duck_pill = page.locator('[data-facet-field="species"][data-facet-value="duck"]')
|
||||
expect(duck_pill).to_be_visible()
|
||||
duck_pill.click()
|
||||
|
||||
# Filter input should now contain species:duck
|
||||
filter_input = page.locator("#filter")
|
||||
expect(filter_input).to_have_value("species:duck")
|
||||
|
||||
|
||||
class TestFacetPillsOnTagAddForm:
|
||||
"""Test facet pills functionality on the tag add form."""
|
||||
|
||||
def test_facet_pills_visible_on_tag_add_page(self, page: Page, live_server):
|
||||
"""Verify facet pills section is visible on tag add page."""
|
||||
page.goto(f"{live_server.url}/actions/tag-add")
|
||||
expect(page.locator("body")).to_be_visible()
|
||||
|
||||
# Should see facet pills container
|
||||
facet_container = page.locator("#dsl-facet-pills")
|
||||
expect(facet_container).to_be_visible()
|
||||
|
||||
|
||||
class TestFacetPillsOnRegistry:
|
||||
"""Test facet pills on registry replace existing facets."""
|
||||
|
||||
def test_registry_facet_pills_visible(self, page: Page, live_server):
|
||||
"""Verify facet pills appear in registry sidebar."""
|
||||
page.goto(f"{live_server.url}/registry")
|
||||
expect(page.locator("body")).to_be_visible()
|
||||
|
||||
# Should see facet pills in sidebar
|
||||
facet_container = page.locator("#dsl-facet-pills")
|
||||
expect(facet_container).to_be_visible()
|
||||
|
||||
def test_registry_facet_click_updates_filter(self, page: Page, live_server):
|
||||
"""Clicking a facet in registry updates the filter."""
|
||||
page.goto(f"{live_server.url}/registry")
|
||||
expect(page.locator("body")).to_be_visible()
|
||||
|
||||
# Click on species facet
|
||||
duck_pill = page.locator('[data-facet-field="species"][data-facet-value="duck"]')
|
||||
expect(duck_pill).to_be_visible()
|
||||
duck_pill.click()
|
||||
|
||||
# Filter input should be updated
|
||||
filter_input = page.locator("#filter")
|
||||
expect(filter_input).to_have_value("species:duck")
|
||||
|
||||
|
||||
class TestSelectDarkMode:
|
||||
"""Test select dropdown visibility in dark mode."""
|
||||
|
||||
def test_select_options_visible_on_move_form(self, page: Page, live_server):
|
||||
"""Verify select dropdown options are readable in dark mode."""
|
||||
page.goto(f"{live_server.url}/move")
|
||||
expect(page.locator("body")).to_be_visible()
|
||||
|
||||
# Click to open destination dropdown
|
||||
select = page.locator("#to_location_id")
|
||||
expect(select).to_be_visible()
|
||||
|
||||
# Check the select has proper dark mode styling
|
||||
# Note: We check computed styles to verify color-scheme is set
|
||||
color_scheme = select.evaluate("el => window.getComputedStyle(el).colorScheme")
|
||||
# Should have dark color scheme for native dark mode option styling
|
||||
assert "dark" in color_scheme.lower() or color_scheme == "auto"
|
||||
|
||||
def test_outcome_select_options_visible(self, page: Page, live_server):
|
||||
"""Verify outcome dropdown options are readable."""
|
||||
page.goto(f"{live_server.url}/actions/outcome")
|
||||
expect(page.locator("body")).to_be_visible()
|
||||
|
||||
# Check outcome dropdown has proper styling
|
||||
select = page.locator("#outcome")
|
||||
expect(select).to_be_visible()
|
||||
|
||||
# Verify the select can be interacted with
|
||||
select.click()
|
||||
expect(select).to_be_focused()
|
||||
195
tests/test_api_facets.py
Normal file
195
tests/test_api_facets.py
Normal file
@@ -0,0 +1,195 @@
|
||||
# ABOUTME: Unit tests for /api/facets endpoint.
|
||||
# ABOUTME: Tests dynamic facet count retrieval based on filter.
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from animaltrack.events.payloads import AnimalCohortCreatedPayload
|
||||
from animaltrack.events.store import EventStore
|
||||
from animaltrack.projections import ProjectionRegistry
|
||||
from animaltrack.projections.animal_registry import AnimalRegistryProjection
|
||||
from animaltrack.projections.event_animals import EventAnimalsProjection
|
||||
from animaltrack.projections.intervals import IntervalProjection
|
||||
from animaltrack.services.animal import AnimalService
|
||||
|
||||
|
||||
def make_test_settings(
|
||||
csrf_secret: str = "test-secret",
|
||||
trusted_proxy_ips: str = "127.0.0.1",
|
||||
dev_mode: bool = True,
|
||||
):
|
||||
"""Create Settings for testing by setting env vars temporarily."""
|
||||
from animaltrack.config import Settings
|
||||
|
||||
old_env = os.environ.copy()
|
||||
try:
|
||||
os.environ["CSRF_SECRET"] = csrf_secret
|
||||
os.environ["TRUSTED_PROXY_IPS"] = trusted_proxy_ips
|
||||
os.environ["DEV_MODE"] = str(dev_mode).lower()
|
||||
return Settings()
|
||||
finally:
|
||||
os.environ.clear()
|
||||
os.environ.update(old_env)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(seeded_db):
|
||||
"""Create a test client for the app."""
|
||||
from animaltrack.web.app import create_app
|
||||
|
||||
settings = make_test_settings(trusted_proxy_ips="testclient")
|
||||
app, rt = create_app(settings=settings, db=seeded_db)
|
||||
return TestClient(app, raise_server_exceptions=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def projection_registry(seeded_db):
|
||||
"""Create a ProjectionRegistry with animal projections registered."""
|
||||
registry = ProjectionRegistry()
|
||||
registry.register(AnimalRegistryProjection(seeded_db))
|
||||
registry.register(EventAnimalsProjection(seeded_db))
|
||||
registry.register(IntervalProjection(seeded_db))
|
||||
return registry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def animal_service(seeded_db, projection_registry):
|
||||
"""Create an AnimalService for testing."""
|
||||
event_store = EventStore(seeded_db)
|
||||
return AnimalService(seeded_db, event_store, projection_registry)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def location_strip1_id(seeded_db):
|
||||
"""Get Strip 1 location ID from seeded data."""
|
||||
row = seeded_db.execute("SELECT id FROM locations WHERE name = 'Strip 1'").fetchone()
|
||||
return row[0]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def location_strip2_id(seeded_db):
|
||||
"""Get Strip 2 location ID from seeded data."""
|
||||
row = seeded_db.execute("SELECT id FROM locations WHERE name = 'Strip 2'").fetchone()
|
||||
return row[0]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ducks_at_strip1(seeded_db, animal_service, location_strip1_id):
|
||||
"""Create 5 female ducks at Strip 1."""
|
||||
payload = AnimalCohortCreatedPayload(
|
||||
species="duck",
|
||||
count=5,
|
||||
life_stage="adult",
|
||||
sex="female",
|
||||
location_id=location_strip1_id,
|
||||
origin="purchased",
|
||||
)
|
||||
ts_utc = int(time.time() * 1000)
|
||||
event = animal_service.create_cohort(payload, ts_utc, "test_user")
|
||||
return event.entity_refs["animal_ids"]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def geese_at_strip2(seeded_db, animal_service, location_strip2_id):
|
||||
"""Create 3 male geese at Strip 2."""
|
||||
payload = AnimalCohortCreatedPayload(
|
||||
species="goose",
|
||||
count=3,
|
||||
life_stage="adult",
|
||||
sex="male",
|
||||
location_id=location_strip2_id,
|
||||
origin="purchased",
|
||||
)
|
||||
ts_utc = int(time.time() * 1000)
|
||||
event = animal_service.create_cohort(payload, ts_utc, "test_user")
|
||||
return event.entity_refs["animal_ids"]
|
||||
|
||||
|
||||
class TestApiFacetsEndpoint:
|
||||
"""Test GET /api/facets endpoint."""
|
||||
|
||||
def test_facets_endpoint_exists(self, client, ducks_at_strip1):
|
||||
"""Verify the facets endpoint responds."""
|
||||
response = client.get("/api/facets")
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_facets_returns_html_partial(self, client, ducks_at_strip1):
|
||||
"""Facets endpoint returns HTML partial for HTMX swap."""
|
||||
response = client.get("/api/facets")
|
||||
assert response.status_code == 200
|
||||
content = response.text
|
||||
|
||||
# Should be HTML with facet pills structure
|
||||
assert 'id="dsl-facet-pills"' in content
|
||||
assert "Species" in content
|
||||
|
||||
def test_facets_respects_filter(self, client, ducks_at_strip1, geese_at_strip2):
|
||||
"""Facets endpoint applies filter and shows filtered counts."""
|
||||
# Get facets filtered to ducks only
|
||||
response = client.get("/api/facets?filter=species:duck")
|
||||
assert response.status_code == 200
|
||||
content = response.text
|
||||
|
||||
# Should show sex facets for ducks (5 female)
|
||||
assert "female" in content.lower()
|
||||
# Should not show goose sex (male) since we filtered to ducks
|
||||
# (actually it might show male=0 or not at all)
|
||||
|
||||
def test_facets_shows_count_for_alive_animals(self, client, ducks_at_strip1):
|
||||
"""Facets show counts for alive animals by default."""
|
||||
response = client.get("/api/facets")
|
||||
assert response.status_code == 200
|
||||
content = response.text
|
||||
|
||||
# Should show species with counts
|
||||
assert "duck" in content.lower() or "Duck" in content
|
||||
# Count 5 should appear
|
||||
assert "5" in content
|
||||
|
||||
def test_facets_with_empty_filter(self, client, ducks_at_strip1, geese_at_strip2):
|
||||
"""Empty filter returns all alive animals' facets."""
|
||||
response = client.get("/api/facets?filter=")
|
||||
assert response.status_code == 200
|
||||
content = response.text
|
||||
|
||||
# Should have facet pills
|
||||
assert 'id="dsl-facet-pills"' in content
|
||||
|
||||
def test_facets_with_location_filter(self, client, ducks_at_strip1, geese_at_strip2):
|
||||
"""Location filter shows facets for that location only."""
|
||||
response = client.get('/api/facets?filter=location:"Strip 1"')
|
||||
assert response.status_code == 200
|
||||
content = response.text
|
||||
|
||||
# Should show ducks (at Strip 1)
|
||||
assert "duck" in content.lower() or "Duck" in content
|
||||
|
||||
def test_facets_includes_htmx_swap_attributes(self, client, ducks_at_strip1):
|
||||
"""Returned HTML has proper ID for HTMX swap targeting."""
|
||||
response = client.get("/api/facets")
|
||||
assert response.status_code == 200
|
||||
content = response.text
|
||||
|
||||
# Must have same ID for outerHTML swap to work
|
||||
assert 'id="dsl-facet-pills"' in content
|
||||
|
||||
|
||||
class TestApiFacetsWithSelectionPreview:
|
||||
"""Test facets endpoint integrates with selection preview workflow."""
|
||||
|
||||
def test_facets_and_preview_use_same_filter(self, client, ducks_at_strip1, geese_at_strip2):
|
||||
"""Both endpoints interpret the same filter consistently."""
|
||||
filter_str = "species:duck"
|
||||
|
||||
# Get facets
|
||||
facets_resp = client.get(f"/api/facets?filter={filter_str}")
|
||||
assert facets_resp.status_code == 200
|
||||
|
||||
# Get selection preview
|
||||
preview_resp = client.get(f"/api/selection-preview?filter={filter_str}")
|
||||
assert preview_resp.status_code == 200
|
||||
|
||||
# Both should work with the same filter
|
||||
233
tests/test_dsl_facets.py
Normal file
233
tests/test_dsl_facets.py
Normal file
@@ -0,0 +1,233 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user