feat: US-005 - Frontend: Stats section collapse with chevron
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user