feat: US-005 - Frontend: Stats section collapse with chevron

This commit is contained in:
Echo
2026-02-10 19:07:21 +00:00
parent f3aa97c910
commit 9a899f94fd
2 changed files with 294 additions and 1 deletions

View File

@@ -1696,9 +1696,29 @@
if (content.classList.contains('visible')) {
content.classList.remove('visible');
chevron.classList.remove('expanded');
// Save collapsed state to localStorage
localStorage.setItem('habits-stats-collapsed', 'true');
} else {
content.classList.add('visible');
chevron.classList.add('expanded');
// Save expanded state to localStorage
localStorage.setItem('habits-stats-collapsed', 'false');
}
}
function restoreWeeklySummaryState() {
const content = document.getElementById('weeklySummaryContent');
const chevron = document.getElementById('weeklySummaryChevron');
const isCollapsed = localStorage.getItem('habits-stats-collapsed');
// Default is collapsed (isCollapsed === null means first visit)
// Only expand if explicitly set to 'false'
if (isCollapsed === 'false') {
content.classList.add('visible');
chevron.classList.add('expanded');
} else {
content.classList.remove('visible');
chevron.classList.remove('expanded');
}
}
@@ -2452,6 +2472,9 @@
}
});
// Restore collapsed/expanded state from localStorage
restoreWeeklySummaryState();
loadHabits();
</script>
</body>

View File

@@ -2,6 +2,7 @@
Test suite for Habits frontend page structure and navigation
Story US-002: Frontend - Compact habit cards (~100px height)
Story US-003: Frontend - Check/uncheck toggle behavior
Story US-005: Frontend - Stats section collapse with chevron
Story US-006: Frontend - Page structure, layout, and navigation link
Story US-007: Frontend - Habit card component
Story US-008: Frontend - Create habit modal with all options
@@ -1752,9 +1753,23 @@ def run_all_tests():
test_render_habits_uses_search,
test_event_listeners_initialized,
test_typecheck_us004,
# US-005 tests (Stats section collapse)
test_stats_section_collapsed_by_default,
test_stats_header_clickable_with_chevron,
test_toggle_function_exists,
test_chevron_rotates_on_expand,
test_content_displays_when_visible,
test_localstorage_save_on_toggle,
test_restore_function_exists,
test_restore_expands_when_false,
test_restore_collapses_by_default,
test_restore_called_on_page_load,
test_css_transition_300ms,
test_height_constraint_collapsed,
test_typecheck_us005,
]
print(f"\nRunning {len(tests)} frontend tests for US-002, US-003, US-006 through US-014...\n")
print(f"\nRunning {len(tests)} frontend tests for US-002, US-003, US-005, US-006 through US-014...\n")
failed = []
for test in tests:
@@ -2258,5 +2273,260 @@ def test_typecheck_us004():
assert result.returncode == 0, f"Typecheck failed: {result.stderr}"
print("✓ Test 137: Typecheck passes")
# ========== US-005 Tests: Stats section collapse with chevron ==========
def test_stats_section_collapsed_by_default():
"""Test 138: Stats section (weekly summary) starts collapsed by default"""
html_path = Path(__file__).parent.parent / 'habits.html'
content = html_path.read_text()
# Check that weekly-summary-content does NOT have 'visible' class by default in HTML
assert '<div class="weekly-summary-content" id="weeklySummaryContent">' in content or \
'<div class="weekly-summary-content"' in content, \
"weekly-summary-content should not have 'visible' class in HTML"
# Check CSS: content should be display:none when not visible
assert '.weekly-summary-content {' in content, "Should have weekly-summary-content CSS"
assert 'display: none' in content, "Content should be display:none by default"
print("✓ Test 138: Stats section starts collapsed (display:none)")
def test_stats_header_clickable_with_chevron():
"""Test 139: Stats header is clickable and has chevron icon"""
html_path = Path(__file__).parent.parent / 'habits.html'
content = html_path.read_text()
# Check header has onclick handler
assert 'onclick="toggleWeeklySummary()"' in content, \
"Header should have onclick='toggleWeeklySummary()'"
# Check for chevron icon
assert 'weekly-summary-chevron' in content, "Should have chevron element"
assert 'data-lucide="chevron-down"' in content, "Should use Lucide chevron-down icon"
# Check header is styled as clickable
assert 'cursor: pointer' in content, "Header should have cursor:pointer"
print("✓ Test 139: Stats header is clickable with chevron icon")
def test_toggle_function_exists():
"""Test 140: toggleWeeklySummary function exists and toggles visibility"""
html_path = Path(__file__).parent.parent / 'habits.html'
content = html_path.read_text()
# Check function exists
assert 'function toggleWeeklySummary()' in content, \
"toggleWeeklySummary function should be defined"
# Find function body
func_start = content.find('function toggleWeeklySummary()')
func_end = content.find('\n }', func_start + 1000)
func_body = content[func_start:func_end]
# Check it toggles visibility class
assert 'classList.contains(\'visible\')' in func_body, \
"Should check if content has 'visible' class"
assert 'classList.remove(\'visible\')' in func_body, \
"Should remove 'visible' class when collapsing"
assert 'classList.add(\'visible\')' in func_body, \
"Should add 'visible' class when expanding"
# Check it toggles chevron class
assert 'classList.remove(\'expanded\')' in func_body, \
"Should remove 'expanded' class from chevron when collapsing"
assert 'classList.add(\'expanded\')' in func_body, \
"Should add 'expanded' class to chevron when expanding"
print("✓ Test 140: toggleWeeklySummary toggles visible and expanded classes")
def test_chevron_rotates_on_expand():
"""Test 141: Chevron rotates 180deg when expanded"""
html_path = Path(__file__).parent.parent / 'habits.html'
content = html_path.read_text()
# Check for chevron rotation CSS
assert '.weekly-summary-chevron.expanded' in content, \
"Should have .expanded class styles for chevron"
assert 'transform: rotate(180deg)' in content, \
"Chevron should rotate 180deg when expanded"
# Check for transition
assert 'transition:' in content.lower(), "Chevron should have transition"
print("✓ Test 141: Chevron rotates 180deg when expanded")
def test_content_displays_when_visible():
"""Test 142: Content displays (display:block) when visible class is added"""
html_path = Path(__file__).parent.parent / 'habits.html'
content = html_path.read_text()
# Check for .visible class CSS
assert '.weekly-summary-content.visible' in content, \
"Should have CSS for .visible class"
assert 'display: block' in content, \
"Content should be display:block when visible"
print("✓ Test 142: Content displays when visible class is added")
def test_localstorage_save_on_toggle():
"""Test 143: toggleWeeklySummary saves state to localStorage"""
html_path = Path(__file__).parent.parent / 'habits.html'
content = html_path.read_text()
# Find toggleWeeklySummary function
func_start = content.find('function toggleWeeklySummary()')
func_end = content.find('\n }', func_start + 1500)
func_body = content[func_start:func_end]
# Check it saves to localStorage
assert 'localStorage.setItem' in func_body, \
"Should save state to localStorage"
assert "'habits-stats-collapsed'" in func_body or '"habits-stats-collapsed"' in func_body, \
"Should use 'habits-stats-collapsed' key"
assert "'true'" in func_body or '"true"' in func_body, \
"Should save 'true' for collapsed state"
assert "'false'" in func_body or '"false"' in func_body, \
"Should save 'false' for expanded state"
print("✓ Test 143: toggleWeeklySummary saves state to localStorage")
def test_restore_function_exists():
"""Test 144: restoreWeeklySummaryState function exists and loads from localStorage"""
html_path = Path(__file__).parent.parent / 'habits.html'
content = html_path.read_text()
# Check function exists
assert 'function restoreWeeklySummaryState()' in content, \
"restoreWeeklySummaryState function should be defined"
# Find function body
func_start = content.find('function restoreWeeklySummaryState()')
func_end = content.find('\n }', func_start + 1000)
func_body = content[func_start:func_end]
# Check it loads from localStorage
assert 'localStorage.getItem' in func_body, \
"Should load state from localStorage"
assert "'habits-stats-collapsed'" in func_body or '"habits-stats-collapsed"' in func_body, \
"Should use 'habits-stats-collapsed' key"
print("✓ Test 144: restoreWeeklySummaryState loads from localStorage")
def test_restore_expands_when_false():
"""Test 145: restoreWeeklySummaryState expands when localStorage is 'false'"""
html_path = Path(__file__).parent.parent / 'habits.html'
content = html_path.read_text()
# Find restore function
func_start = content.find('function restoreWeeklySummaryState()')
func_end = content.find('\n }', func_start + 1000)
func_body = content[func_start:func_end]
# Check it expands when isCollapsed === 'false'
assert "isCollapsed === 'false'" in func_body or 'isCollapsed==="false"' in func_body or \
"isCollapsed === \"false\"" in func_body, \
"Should check if isCollapsed is 'false'"
assert 'classList.add(\'visible\')' in func_body, \
"Should add 'visible' class when expanding"
assert 'classList.add(\'expanded\')' in func_body, \
"Should add 'expanded' class to chevron when expanding"
print("✓ Test 145: restoreWeeklySummaryState expands when localStorage is 'false'")
def test_restore_collapses_by_default():
"""Test 146: restoreWeeklySummaryState keeps collapsed when localStorage is null or 'true'"""
html_path = Path(__file__).parent.parent / 'habits.html'
content = html_path.read_text()
# Find restore function
func_start = content.find('function restoreWeeklySummaryState()')
func_end = content.find('\n }', func_start + 1000)
func_body = content[func_start:func_end]
# Check it handles null/true cases (else branch or explicit check)
assert 'else' in func_body, "Should have else branch for collapsed state"
assert 'classList.remove(\'visible\')' in func_body, \
"Should remove 'visible' class for collapsed state"
assert 'classList.remove(\'expanded\')' in func_body, \
"Should remove 'expanded' class from chevron for collapsed state"
print("✓ Test 146: restoreWeeklySummaryState keeps collapsed by default")
def test_restore_called_on_page_load():
"""Test 147: restoreWeeklySummaryState is called on page load"""
html_path = Path(__file__).parent.parent / 'habits.html'
content = html_path.read_text()
# Check that restore function is called in initialization section
# It should be called before or near loadHabits()
init_section = content[-3000:] # Last 3000 chars
assert 'restoreWeeklySummaryState()' in init_section, \
"Should call restoreWeeklySummaryState() on page load"
# Check it's called before loadHabits (or at least in the init section)
restore_pos = init_section.find('restoreWeeklySummaryState()')
load_pos = init_section.find('loadHabits()')
assert restore_pos < load_pos, \
"restoreWeeklySummaryState should be called before loadHabits"
print("✓ Test 147: restoreWeeklySummaryState called on page load")
def test_css_transition_300ms():
"""Test 148: CSS uses 300ms ease transition for animations"""
html_path = Path(__file__).parent.parent / 'habits.html'
content = html_path.read_text()
# Find weekly-summary CSS sections
chevron_css_start = content.find('.weekly-summary-chevron')
chevron_css_end = content.find('}', chevron_css_start)
chevron_css = content[chevron_css_start:chevron_css_end]
# Check for transition
assert 'transition:' in chevron_css.lower(), \
"Chevron should have transition"
# The transition should be in the base CSS (using var(--transition-base) or explicit)
# var(--transition-base) is typically 300ms ease from common.css
assert 'var(--transition-base)' in chevron_css or '0.3s' in chevron_css or '300ms' in chevron_css, \
"Should use 300ms transition (or --transition-base variable)"
print("✓ Test 148: CSS uses transition for smooth animation")
def test_height_constraint_collapsed():
"""Test 149: Collapsed state shows minimal height (~30px header only)"""
html_path = Path(__file__).parent.parent / 'habits.html'
content = html_path.read_text()
# Check header padding/height
header_css_start = content.find('.weekly-summary-header {')
header_css_end = content.find('}', header_css_start)
header_css = content[header_css_start:header_css_end]
# Header should have padding to create ~30px height
assert 'padding:' in header_css.lower(), "Header should have padding"
# Content should be hidden (display:none) when not visible
content_css_start = content.find('.weekly-summary-content {')
content_css_end = content.find('}', content_css_start)
content_css = content[content_css_start:content_css_end]
assert 'display: none' in content_css or 'display:none' in content_css, \
"Content should be display:none when collapsed"
print("✓ Test 149: Collapsed state shows minimal height (header only)")
def test_typecheck_us005():
"""Test 150: Typecheck passes for US-005 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 150: Typecheck passes")
if __name__ == '__main__':
run_all_tests()