feat: US-004 - Frontend: Search and filter collapse to icons

This commit is contained in:
Echo
2026-02-10 18:59:45 +00:00
parent 9d9f00e069
commit f3aa97c910
2 changed files with 484 additions and 39 deletions

View File

@@ -1520,16 +1520,17 @@ def test_pickers_wrap_mobile():
print("✓ Test 94: Icon and color pickers wrap properly on mobile")
def test_filter_bar_stacks_mobile():
"""Test 95: Filter bar stacks vertically on mobile"""
"""Test 95: Filter options stack vertically on mobile"""
habits_path = Path(__file__).parent.parent / 'habits.html'
content = habits_path.read_text()
# Check for filter bar mobile styles
# Check for filter options mobile styles
mobile_section = content[content.find('@media (max-width: 768px)'):content.find('@media (max-width: 768px)') + 3000]
assert '.filter-bar' in mobile_section, "Should have filter-bar mobile styles"
assert 'flex-direction: column' in mobile_section, "Filter bar should stack vertically"
print("✓ Test 95: Filter bar stacks vertically on mobile")
assert '.filter-options' in mobile_section or '.filter-group' in mobile_section, \
"Should have filter options mobile styles"
assert 'flex-direction: column' in mobile_section, "Filter options should stack vertically"
print("✓ Test 95: Filter options stack vertically on mobile")
def test_stats_2x2_mobile():
"""Test 96: Stats row shows 2x2 grid on mobile"""
@@ -1737,6 +1738,20 @@ def run_all_tests():
test_toast_messages_for_toggle,
test_delete_request_format,
test_typecheck_us003,
# US-004 tests (Search/filter collapse)
test_filter_toolbar_icons,
test_search_container_collapsible,
test_filter_container_collapsible,
test_toggle_functions_exist,
test_toggle_logic,
test_css_animations,
test_esc_key_listener,
test_click_outside_listener,
test_search_function_exists,
test_search_input_listener,
test_render_habits_uses_search,
test_event_listeners_initialized,
test_typecheck_us004,
]
print(f"\nRunning {len(tests)} frontend tests for US-002, US-003, US-006 through US-014...\n")
@@ -2014,5 +2029,234 @@ def test_typecheck_us003():
assert result.returncode == 0, f"Typecheck failed: {result.stderr}"
print("✓ Test 124: Typecheck passes")
# ========================================
# US-004: Frontend - Search and filter collapse to icons
# ========================================
def test_filter_toolbar_icons():
"""Test 125: Filter bar shows icon toolbar on page load (~40px height)"""
habits_path = Path(__file__).parent.parent / 'habits.html'
content = habits_path.read_text()
# Check for toolbar structure
assert 'filter-toolbar' in content, "Should have filter-toolbar element"
assert 'id="searchToggle"' in content, "Should have search toggle button"
assert 'id="filterToggle"' in content, "Should have filter toggle button"
assert 'data-lucide="search"' in content, "Should have search icon"
assert 'data-lucide="sliders"' in content or 'data-lucide="filter"' in content, \
"Should have filter/sliders icon"
# Check toolbar height
assert 'min-height: 40px' in content or 'min-height:40px' in content, \
"Toolbar should be ~40px height"
print("✓ Test 125: Filter toolbar with icons exists")
def test_search_container_collapsible():
"""Test 126: Search container is collapsible with expanded class"""
habits_path = Path(__file__).parent.parent / 'habits.html'
content = habits_path.read_text()
# Check for search container structure
assert 'search-container' in content, "Should have search-container element"
assert 'id="searchContainer"' in content, "Search container should have id"
assert 'id="searchInput"' in content, "Should have search input"
# Check CSS for collapse/expand states
assert 'max-height: 0' in content, "Collapsed state should have max-height: 0"
assert '.search-container.expanded' in content or '.expanded' in content, \
"Should have expanded state CSS"
print("✓ Test 126: Search container is collapsible")
def test_filter_container_collapsible():
"""Test 127: Filter container is collapsible with expanded class"""
habits_path = Path(__file__).parent.parent / 'habits.html'
content = habits_path.read_text()
# Check for filter container structure
assert 'filter-container' in content, "Should have filter-container element"
assert 'id="filterContainer"' in content, "Filter container should have id"
assert 'filter-options' in content, "Should have filter-options wrapper"
# Check that filter options are inside container
filter_start = content.find('id="filterContainer"')
category_pos = content.find('id="categoryFilter"')
status_pos = content.find('id="statusFilter"')
sort_pos = content.find('id="sortSelect"')
assert filter_start < category_pos, "Category filter should be inside filter container"
assert filter_start < status_pos, "Status filter should be inside filter container"
assert filter_start < sort_pos, "Sort select should be inside filter container"
print("✓ Test 127: Filter container is collapsible")
def test_toggle_functions_exist():
"""Test 128: toggleSearch, toggleFilters, and collapseAll functions defined"""
habits_path = Path(__file__).parent.parent / 'habits.html'
js = habits_path.read_text()
assert 'function toggleSearch()' in js, "Should have toggleSearch function"
assert 'function toggleFilters()' in js, "Should have toggleFilters function"
assert 'function collapseAll()' in js, "Should have collapseAll function"
print("✓ Test 128: Toggle functions are defined")
def test_toggle_logic():
"""Test 129: Toggle functions add/remove 'expanded' class"""
habits_path = Path(__file__).parent.parent / 'habits.html'
js = habits_path.read_text()
# Extract toggleSearch function
toggle_start = js.find('function toggleSearch()')
toggle_end = js.find('function toggleFilters()', toggle_start)
toggle_search_code = js[toggle_start:toggle_end]
assert "classList.contains('expanded')" in toggle_search_code, \
"Should check for expanded state"
assert "classList.add('expanded')" in toggle_search_code, \
"Should add expanded class"
assert "classList.remove('expanded')" in toggle_search_code, \
"Should remove expanded class"
assert "classList.add('active')" in toggle_search_code, \
"Should add active class to button"
print("✓ Test 129: Toggle functions implement expand/collapse logic")
def test_css_animations():
"""Test 130: CSS includes transition animations (300ms ease)"""
habits_path = Path(__file__).parent.parent / 'habits.html'
css = habits_path.read_text()
# Check for transition properties
assert 'transition:' in css or 'transition :' in css, "Should have CSS transitions"
assert '300ms' in css, "Should have 300ms transition duration"
assert 'ease' in css, "Should use ease timing function"
# Check for max-height transition specifically
assert 'max-height' in css, "Should animate max-height"
print("✓ Test 130: CSS animations defined (300ms ease)")
def test_esc_key_listener():
"""Test 131: ESC key collapses expanded search/filter"""
habits_path = Path(__file__).parent.parent / 'habits.html'
js = habits_path.read_text()
assert "addEventListener('keydown'" in js, "Should have keydown event listener"
assert "e.key === 'Escape'" in js or 'e.key === "Escape"' in js, \
"Should check for Escape key"
assert 'collapseAll()' in js, "Should call collapseAll on ESC"
print("✓ Test 131: ESC key listener implemented")
def test_click_outside_listener():
"""Test 132: Clicking outside collapses expanded search/filter"""
habits_path = Path(__file__).parent.parent / 'habits.html'
js = habits_path.read_text()
# Find click outside logic
assert "addEventListener('click'" in js, "Should have click event listener"
assert '.filter-bar' in js, "Should reference filter-bar element"
assert 'contains(e.target)' in js, "Should check if click is outside"
# Check that it calls collapseAll when clicked outside
# Look for the pattern in the entire file since it might be structured across lines
click_listeners = []
pos = 0
while True:
pos = js.find("addEventListener('click'", pos)
if pos == -1:
break
click_listeners.append(js[pos:pos + 800])
pos += 1
# Check if any click listener has the collapse logic
has_collapse = any('collapseAll' in listener for listener in click_listeners)
assert has_collapse, "Should call collapseAll when clicking outside"
print("✓ Test 132: Click outside listener implemented")
def test_search_function_exists():
"""Test 133: searchHabits function filters by name"""
habits_path = Path(__file__).parent.parent / 'habits.html'
js = habits_path.read_text()
assert 'function searchHabits(' in js, "Should have searchHabits function"
# Check search logic
search_start = js.find('function searchHabits(')
search_end = js.find('function ', search_start + 20)
search_code = js[search_start:search_end]
assert 'toLowerCase()' in search_code, "Should use case-insensitive search"
assert 'includes(' in search_code, "Should use includes for matching"
assert 'habit.name' in search_code, "Should search by habit name"
print("✓ Test 133: searchHabits function filters by name")
def test_search_input_listener():
"""Test 134: Search input triggers renderHabits on input event"""
habits_path = Path(__file__).parent.parent / 'habits.html'
js = habits_path.read_text()
# Check for search input event listener
assert "getElementById('searchInput')" in js or 'getElementById("searchInput")' in js, \
"Should get searchInput element"
assert "addEventListener('input'" in js, "Should listen to input events"
assert 'renderHabits()' in js, "Should call renderHabits when searching"
print("✓ Test 134: Search input listener triggers renderHabits")
def test_render_habits_uses_search():
"""Test 135: renderHabits applies searchHabits before filters"""
habits_path = Path(__file__).parent.parent / 'habits.html'
js = habits_path.read_text()
# Find renderHabits function
render_start = js.find('function renderHabits()')
render_section = js[render_start:render_start + 2000]
assert 'searchHabits(' in render_section, "Should call searchHabits"
assert 'filterHabits(' in render_section, "Should call filterHabits"
# Check that search happens before filter
search_pos = render_section.find('searchHabits(')
filter_pos = render_section.find('filterHabits(')
assert search_pos < filter_pos, "Search should happen before filtering"
print("✓ Test 135: renderHabits applies search before filters")
def test_event_listeners_initialized():
"""Test 136: Toggle button event listeners attached on page load"""
habits_path = Path(__file__).parent.parent / 'habits.html'
js = habits_path.read_text()
# Find initialization section (near end of script)
init_section = js[-2000:] # Last 2000 chars
assert "getElementById('searchToggle')" in init_section or \
'getElementById("searchToggle")' in init_section, \
"Should attach listener to searchToggle"
assert "getElementById('filterToggle')" in init_section or \
'getElementById("filterToggle")' in init_section, \
"Should attach listener to filterToggle"
assert 'toggleSearch' in init_section, "Should attach toggleSearch handler"
assert 'toggleFilters' in init_section, "Should attach toggleFilters handler"
print("✓ Test 136: Event listeners initialized on page load")
def test_typecheck_us004():
"""Test 137: Typecheck passes for US-004 changes"""
repo_root = Path(__file__).parent.parent.parent
result = subprocess.run(
['python3', '-m', 'py_compile', 'dashboard/api.py', 'dashboard/habits_helpers.py'],
cwd=repo_root,
capture_output=True,
text=True
)
assert result.returncode == 0, f"Typecheck failed: {result.stderr}"
print("✓ Test 137: Typecheck passes")
if __name__ == '__main__':
run_all_tests()