2533 lines
116 KiB
Python
2533 lines
116 KiB
Python
"""
|
|
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
|
|
Story US-009: Frontend - Edit habit modal
|
|
Story US-010: Frontend - Check-in interaction (click and long-press)
|
|
Story US-011: Frontend - Skip, lives display, and delete confirmation
|
|
Story US-012: Frontend - Filter and sort controls
|
|
Story US-013: Frontend - Stats section and weekly summary
|
|
Story US-014: Frontend - Mobile responsive and touch optimization
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
# Add parent directory to path for imports
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
def test_habits_html_exists():
|
|
"""Test 1: habits.html exists in dashboard/"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
assert habits_path.exists(), "habits.html should exist in dashboard/"
|
|
print("✓ Test 1: habits.html exists")
|
|
|
|
def test_habits_html_structure():
|
|
"""Test 2: Page includes common.css, Lucide icons, and swipe-nav.js"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'href="/echo/common.css"' in content, "Should include common.css"
|
|
assert 'lucide@latest/dist/umd/lucide.min.js' in content, "Should include Lucide icons"
|
|
assert 'src="/echo/swipe-nav.js"' in content, "Should include swipe-nav.js"
|
|
print("✓ Test 2: Page includes required CSS and JS")
|
|
|
|
def test_page_has_header():
|
|
"""Test 3: Page has header with 'Habits' title and 'Add Habit' button"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'class="page-title"' in content, "Should have page-title element"
|
|
assert '>Habits</' in content, "Should have 'Habits' title"
|
|
assert 'Add Habit' in content, "Should have 'Add Habit' button"
|
|
assert 'showAddHabitModal()' in content, "Add Habit button should have onclick handler"
|
|
print("✓ Test 3: Page has header with title and Add Habit button")
|
|
|
|
def test_empty_state():
|
|
"""Test 4: Empty state message shown when no habits exist"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'No habits yet' in content, "Should have empty state message"
|
|
assert 'Create your first habit' in content, "Should have call-to-action"
|
|
assert 'empty-state' in content, "Should have empty-state class"
|
|
print("✓ Test 4: Empty state message present")
|
|
|
|
def test_grid_container():
|
|
"""Test 5: Grid container uses CSS grid with responsive breakpoints (1/2/3 columns)"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'habits-grid' in content, "Should have habits-grid class"
|
|
assert 'display: grid' in content, "Should use CSS grid"
|
|
assert 'grid-template-columns' in content, "Should define grid columns"
|
|
|
|
# Check responsive breakpoints
|
|
assert '@media (max-width: 768px)' in content or '@media (max-width:768px)' in content, \
|
|
"Should have mobile breakpoint"
|
|
assert 'grid-template-columns: 1fr' in content or 'grid-template-columns:1fr' in content, \
|
|
"Should have 1 column on mobile"
|
|
|
|
# Check for 2 or 3 column layouts
|
|
assert ('grid-template-columns: repeat(2, 1fr)' in content or
|
|
'grid-template-columns:repeat(2,1fr)' in content or
|
|
'grid-template-columns: repeat(3, 1fr)' in content or
|
|
'grid-template-columns:repeat(3,1fr)' in content), \
|
|
"Should have multi-column layout for larger screens"
|
|
|
|
print("✓ Test 5: Grid container with responsive breakpoints")
|
|
|
|
def test_index_navigation_link():
|
|
"""Test 6: index.html navigation includes 'Habits' link with dumbbell icon"""
|
|
index_path = Path(__file__).parent.parent / 'index.html'
|
|
content = index_path.read_text()
|
|
|
|
assert '/echo/habits.html' in content, "Should link to /echo/habits.html"
|
|
assert 'dumbbell' in content, "Should have dumbbell icon"
|
|
assert '>Habits</' in content, "Should have 'Habits' label"
|
|
|
|
# Check that Habits link is in the nav
|
|
nav_start = content.find('<nav class="nav">')
|
|
nav_end = content.find('</nav>', nav_start)
|
|
nav_section = content[nav_start:nav_end]
|
|
|
|
assert '/echo/habits.html' in nav_section, "Habits link should be in navigation"
|
|
assert 'dumbbell' in nav_section, "Dumbbell icon should be in navigation"
|
|
|
|
print("✓ Test 6: index.html includes Habits navigation link")
|
|
|
|
def test_page_fetches_habits():
|
|
"""Test 7: Page fetches GET /echo/api/habits on load"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert "fetch('/echo/api/habits')" in content or 'fetch("/echo/api/habits")' in content, \
|
|
"Should fetch from /echo/api/habits"
|
|
assert 'loadHabits' in content, "Should have loadHabits function"
|
|
|
|
# Check that loadHabits is called on page load
|
|
# (either in inline script or as last statement)
|
|
assert content.count('loadHabits()') > 0, "loadHabits should be called"
|
|
|
|
print("✓ Test 7: Page fetches habits on load")
|
|
|
|
def test_habit_card_rendering():
|
|
"""Test 8: Placeholder habit card rendering exists"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'renderHabitCard' in content, "Should have renderHabitCard function"
|
|
assert 'habit-card' in content, "Should have habit-card class"
|
|
assert 'renderHabits' in content, "Should have renderHabits function"
|
|
|
|
print("✓ Test 8: Habit card rendering functions exist")
|
|
|
|
def test_no_console_errors_structure():
|
|
"""Test 9: No obvious console error sources (basic structure check)"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check for basic script structure
|
|
assert '<script>' in content, "Should have script tags"
|
|
assert 'function' in content, "Should have JavaScript functions"
|
|
|
|
# Check for proper escaping in rendering
|
|
assert 'escapeHtml' in content or 'textContent' in content, \
|
|
"Should have XSS protection (escapeHtml or textContent)"
|
|
|
|
print("✓ Test 9: No obvious console error sources")
|
|
|
|
def test_typecheck():
|
|
"""Test 10: HTML file is well-formed"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Basic HTML structure checks
|
|
assert '<!DOCTYPE html>' in content, "Should have DOCTYPE"
|
|
assert '<html' in content and '</html>' in content, "Should have html tags"
|
|
assert '<head>' in content and '</head>' in content, "Should have head tags"
|
|
assert '<body>' in content and '</body>' in content, "Should have body tags"
|
|
|
|
# Check for matching script tags
|
|
script_open = content.count('<script')
|
|
script_close = content.count('</script>')
|
|
assert script_open == script_close, f"Script tags should match (found {script_open} opens, {script_close} closes)"
|
|
|
|
print("✓ Test 10: HTML structure is well-formed")
|
|
|
|
def test_card_colored_border():
|
|
"""Test 11: Habit card has colored left border matching habit.color"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'border-left-color' in content or 'borderLeftColor' in content, \
|
|
"Card should have colored left border"
|
|
assert 'habit.color' in content, "Card should use habit.color for border"
|
|
print("✓ Test 11: Card has colored left border")
|
|
|
|
def test_card_header_icons():
|
|
"""Test 12: Card header shows icon, name, settings, and delete"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check for icon display
|
|
assert 'habit.icon' in content or 'habit-card-icon' in content, \
|
|
"Card should display habit icon"
|
|
|
|
# Check for name display
|
|
assert 'habit.name' in content or 'habit-card-name' in content, \
|
|
"Card should display habit name"
|
|
|
|
# Check for settings (gear) icon
|
|
assert 'settings' in content.lower(), "Card should have settings icon"
|
|
|
|
# Check for delete (trash) icon
|
|
assert 'trash' in content.lower(), "Card should have delete icon"
|
|
|
|
print("✓ Test 12: Card header has icon, name, settings, and delete")
|
|
|
|
def test_card_streak_display():
|
|
"""Test 13: Streak displays with fire emoji for current and trophy for best"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert '🔥' in content, "Card should have fire emoji for current streak"
|
|
assert '🏆' in content, "Card should have trophy emoji for best streak"
|
|
assert 'habit.streak' in content or 'streak?.current' in content or 'streak.current' in content, \
|
|
"Card should display streak.current"
|
|
assert 'streak?.best' in content or 'streak.best' in content, \
|
|
"Card should display streak.best"
|
|
|
|
print("✓ Test 13: Streak display with fire and trophy emojis")
|
|
|
|
def test_card_checkin_button():
|
|
"""Test 14: Check-in button is large and centered"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'habit-card-check-btn' in content or 'check-btn' in content or 'checkin' in content.lower(), \
|
|
"Card should have check-in button"
|
|
assert 'Check In' in content or 'Check in' in content, \
|
|
"Button should have 'Check In' text"
|
|
|
|
# Check for button styling (large/centered)
|
|
assert 'width: 100%' in content or 'width:100%' in content, \
|
|
"Check-in button should be full-width"
|
|
|
|
print("✓ Test 14: Check-in button is large and centered")
|
|
|
|
def test_card_checkin_disabled_when_done():
|
|
"""Test 15: Check-in button disabled when already checked today"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'disabled' in content, "Button should have disabled state"
|
|
assert 'Done today' in content or 'Done' in content, \
|
|
"Button should show 'Done today' when disabled"
|
|
assert 'isCheckedToday' in content or 'isDoneToday' in content, \
|
|
"Should have function to check if habit is done today"
|
|
|
|
print("✓ Test 15: Check-in button disabled when done today")
|
|
|
|
def test_card_lives_display():
|
|
"""Test 16: Lives display shows filled and empty hearts (total 3)"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert '❤️' in content or '♥' in content, "Card should have filled heart emoji"
|
|
assert '🖤' in content or '♡' in content, "Card should have empty heart emoji"
|
|
assert 'habit.lives' in content or 'renderLives' in content, \
|
|
"Card should display lives"
|
|
|
|
# Check for lives rendering function
|
|
assert 'renderLives' in content or 'lives' in content.lower(), \
|
|
"Should have lives rendering logic"
|
|
|
|
print("✓ Test 16: Lives display with hearts")
|
|
|
|
def test_card_completion_rate():
|
|
"""Test 17: Completion rate percentage is displayed"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'completion_rate' in content or 'completion' in content, \
|
|
"Card should display completion rate"
|
|
assert '(30d)' in content or '30d' in content, \
|
|
"Completion rate should show 30-day period"
|
|
assert '%' in content, "Completion rate should show percentage"
|
|
|
|
print("✓ Test 17: Completion rate displayed")
|
|
|
|
def test_card_footer_category_priority():
|
|
"""Test 18: Footer shows category badge and priority"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'habit.category' in content or 'habit-card-category' in content, \
|
|
"Card should display category"
|
|
assert 'habit.priority' in content or 'priority' in content.lower(), \
|
|
"Card should display priority"
|
|
assert 'habit-card-footer' in content or 'footer' in content.lower(), \
|
|
"Card should have footer section"
|
|
|
|
print("✓ Test 18: Footer shows category and priority")
|
|
|
|
def test_card_lucide_createicons():
|
|
"""Test 19: lucide.createIcons() is called after rendering cards"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check that createIcons is called after rendering
|
|
render_pos = content.find('renderHabits')
|
|
if render_pos != -1:
|
|
after_render = content[render_pos:]
|
|
assert 'lucide.createIcons()' in after_render, \
|
|
"lucide.createIcons() should be called after rendering"
|
|
|
|
print("✓ Test 19: lucide.createIcons() called after rendering")
|
|
|
|
def test_card_common_css_variables():
|
|
"""Test 20: Card uses common.css variables for styling"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check for common.css variable usage
|
|
assert '--bg-surface' in content or '--text-primary' in content or '--border' in content, \
|
|
"Card should use common.css variables"
|
|
assert 'var(--' in content, "Should use CSS variables"
|
|
|
|
print("✓ Test 20: Card uses common.css variables")
|
|
|
|
def test_typecheck_us007():
|
|
"""Test 21: Typecheck passes for US-007"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
assert habits_path.exists(), "habits.html should exist"
|
|
|
|
# Check that all functions are properly defined
|
|
content = habits_path.read_text()
|
|
assert 'function renderHabitCard(' in content, "renderHabitCard function should be defined"
|
|
assert 'function isCheckedToday(' in content, "isCheckedToday function should be defined"
|
|
assert 'function getLastCheckInfo(' in content, "getLastCheckInfo function should be defined"
|
|
assert 'function renderLives(' in content, "renderLives function should be defined"
|
|
assert 'function getPriorityLevel(' in content, "getPriorityLevel function should be defined"
|
|
|
|
print("✓ Test 21: Typecheck passes (all functions defined)")
|
|
|
|
def test_modal_opens_on_add_habit_click():
|
|
"""Test 22: Modal opens when clicking 'Add Habit' button"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'showAddHabitModal()' in content, "Add Habit button should call showAddHabitModal()"
|
|
assert 'function showAddHabitModal(' in content, "showAddHabitModal function should be defined"
|
|
assert 'modal-overlay' in content or 'habitModal' in content, "Should have modal overlay element"
|
|
print("✓ Test 22: Modal opens on Add Habit button click")
|
|
|
|
def test_modal_closes_on_x_and_outside_click():
|
|
"""Test 23: Modal closes on X button or clicking outside"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'closeHabitModal()' in content, "Should have closeHabitModal function"
|
|
assert 'modal-close' in content or 'onclick="closeHabitModal()"' in content, \
|
|
"X button should call closeHabitModal()"
|
|
|
|
# Check for click outside handler
|
|
assert 'e.target === modal' in content or 'event.target' in content, \
|
|
"Should handle clicking outside modal"
|
|
print("✓ Test 23: Modal closes on X button and clicking outside")
|
|
|
|
def test_modal_has_all_form_fields():
|
|
"""Test 24: Form has all required fields"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Required fields
|
|
assert 'habitName' in content or 'name' in content.lower(), "Form should have name field"
|
|
assert 'habitCategory' in content or 'category' in content.lower(), "Form should have category field"
|
|
assert 'habitPriority' in content or 'priority' in content.lower(), "Form should have priority field"
|
|
assert 'habitNotes' in content or 'notes' in content.lower(), "Form should have notes field"
|
|
assert 'frequencyType' in content or 'frequency' in content.lower(), "Form should have frequency field"
|
|
assert 'reminderTime' in content or 'reminder' in content.lower(), "Form should have reminder time field"
|
|
|
|
print("✓ Test 24: Form has all required fields")
|
|
|
|
def test_color_picker_presets_and_custom():
|
|
"""Test 25: Color picker shows preset swatches and custom hex input"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'color-picker' in content or 'colorSwatches' in content or 'color-swatch' in content, \
|
|
"Should have color picker"
|
|
assert 'customColor' in content or 'custom' in content.lower(), \
|
|
"Should have custom color input"
|
|
assert '#RRGGBB' in content or 'pattern=' in content, \
|
|
"Custom color should have hex pattern"
|
|
assert 'presetColors' in content or '#3B82F6' in content or '#EF4444' in content, \
|
|
"Should have preset colors"
|
|
|
|
print("✓ Test 25: Color picker with presets and custom hex")
|
|
|
|
def test_icon_picker_grid():
|
|
"""Test 26: Icon picker shows grid of common Lucide icons"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'icon-picker' in content or 'iconPicker' in content, \
|
|
"Should have icon picker"
|
|
assert 'icon-option' in content or 'commonIcons' in content, \
|
|
"Should have icon options"
|
|
assert 'selectIcon' in content, "Should have selectIcon function"
|
|
|
|
# Check for common icons
|
|
icon_count = sum([1 for icon in ['dumbbell', 'moon', 'book', 'brain', 'heart']
|
|
if icon in content])
|
|
assert icon_count >= 3, "Should have at least 3 common icons"
|
|
|
|
print("✓ Test 26: Icon picker with grid of Lucide icons")
|
|
|
|
def test_frequency_params_conditional():
|
|
"""Test 27: Frequency params display conditionally based on type"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'updateFrequencyParams' in content, "Should have updateFrequencyParams function"
|
|
assert 'frequencyParams' in content, "Should have frequency params container"
|
|
assert 'specific_days' in content, "Should handle specific_days frequency"
|
|
assert 'x_per_week' in content, "Should handle x_per_week frequency"
|
|
assert 'custom' in content.lower(), "Should handle custom frequency"
|
|
|
|
# Check for conditional rendering (day checkboxes for specific_days)
|
|
assert 'day-checkbox' in content or "['Mon', 'Tue'" in content or 'Mon' in content, \
|
|
"Should have day checkboxes for specific_days"
|
|
|
|
print("✓ Test 27: Frequency params display conditionally")
|
|
|
|
def test_client_side_validation():
|
|
"""Test 28: Client-side validation prevents submit without name"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'required' in content, "Name field should be required"
|
|
assert 'trim()' in content, "Should trim input values"
|
|
|
|
# Check for validation in submit function
|
|
submit_func = content[content.find('function submitHabitForm'):]
|
|
assert 'if (!name)' in submit_func or 'name.length' in submit_func, \
|
|
"Should validate name is not empty"
|
|
assert 'showToast' in submit_func and 'error' in submit_func, \
|
|
"Should show error toast for validation failures"
|
|
|
|
print("✓ Test 28: Client-side validation checks name required")
|
|
|
|
def test_submit_posts_to_api():
|
|
"""Test 29: Submit sends POST /echo/api/habits (or PUT for edit) and refreshes list"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'submitHabitForm' in content, "Should have submitHabitForm function"
|
|
|
|
submit_func = content[content.find('function submitHabitForm'):]
|
|
# Check for conditional URL and method (since US-009 added edit support)
|
|
assert ('/echo/api/habits' in submit_func), \
|
|
"Should use /echo/api/habits endpoint"
|
|
assert ("'POST'" in submit_func or '"POST"' in submit_func or "'PUT'" in submit_func or '"PUT"' in submit_func), \
|
|
"Should use POST or PUT method"
|
|
assert 'JSON.stringify' in submit_func, "Should send JSON body"
|
|
assert 'loadHabits()' in submit_func, "Should refresh habit list on success"
|
|
|
|
print("✓ Test 29: Submit POSTs to API and refreshes list")
|
|
|
|
def test_loading_state_on_submit():
|
|
"""Test 30: Loading state shown on submit button during API call"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
submit_func = content[content.find('function submitHabitForm'):]
|
|
assert 'disabled = true' in submit_func or '.disabled' in submit_func, \
|
|
"Submit button should be disabled during API call"
|
|
assert 'Creating' in submit_func or 'loading' in submit_func.lower(), \
|
|
"Should show loading text"
|
|
assert 'disabled = false' in submit_func, \
|
|
"Submit button should be re-enabled after API call"
|
|
|
|
print("✓ Test 30: Loading state on submit button")
|
|
|
|
def test_toast_notifications():
|
|
"""Test 31: Toast notification shown for success and error"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'showToast' in content, "Should have showToast function"
|
|
assert 'toast' in content, "Should have toast styling"
|
|
|
|
toast_func = content[content.find('function showToast'):]
|
|
assert 'success' in toast_func and 'error' in toast_func, \
|
|
"Toast should handle both success and error types"
|
|
assert 'check-circle' in toast_func or 'alert-circle' in toast_func, \
|
|
"Toast should show appropriate icons"
|
|
assert 'setTimeout' in toast_func or 'remove()' in toast_func, \
|
|
"Toast should auto-dismiss"
|
|
|
|
print("✓ Test 31: Toast notifications for success and error")
|
|
|
|
def test_modal_no_console_errors():
|
|
"""Test 32: No obvious console error sources in modal code"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check that modal functions exist
|
|
assert 'function showAddHabitModal(' in content, "showAddHabitModal should be defined"
|
|
assert 'function closeHabitModal(' in content, "closeHabitModal should be defined"
|
|
assert 'function submitHabitForm(' in content, "submitHabitForm should be defined"
|
|
assert 'function updateFrequencyParams(' in content, "updateFrequencyParams should be defined"
|
|
|
|
# Check for proper error handling
|
|
submit_func = content[content.find('function submitHabitForm'):]
|
|
assert 'try' in submit_func and 'catch' in submit_func, \
|
|
"Submit function should have try-catch error handling"
|
|
|
|
print("✓ Test 32: No obvious console error sources")
|
|
|
|
def test_typecheck_us008():
|
|
"""Test 33: Typecheck passes for US-008 (all modal functions defined)"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check all new functions are defined
|
|
required_functions = [
|
|
'showAddHabitModal',
|
|
'closeHabitModal',
|
|
'initColorPicker',
|
|
'selectColor',
|
|
'initIconPicker',
|
|
'selectIcon',
|
|
'updateFrequencyParams',
|
|
'submitHabitForm',
|
|
'showToast'
|
|
]
|
|
|
|
for func in required_functions:
|
|
assert f'function {func}(' in content or f'const {func} =' in content, \
|
|
f"{func} function should be defined"
|
|
|
|
print("✓ Test 33: Typecheck passes (all modal functions defined)")
|
|
|
|
def test_edit_modal_opens_on_gear_icon():
|
|
"""Test 34: Clicking gear icon on habit card opens edit modal"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check that gear icon exists with onclick handler
|
|
assert 'settings' in content, "Should have settings icon (gear)"
|
|
assert "showEditHabitModal" in content, "Should have showEditHabitModal function call"
|
|
|
|
# Check that showEditHabitModal function is defined and not a placeholder
|
|
assert 'function showEditHabitModal(habitId)' in content, "showEditHabitModal should be defined"
|
|
assert 'editingHabitId = habitId' in content or 'editingHabitId=habitId' in content, \
|
|
"Should set editingHabitId"
|
|
assert 'const habit = habits.find(h => h.id === habitId)' in content or \
|
|
'const habit=habits.find(h=>h.id===habitId)' in content, \
|
|
"Should find habit by ID"
|
|
|
|
print("✓ Test 34: Edit modal opens on gear icon click")
|
|
|
|
def test_edit_modal_prepopulated():
|
|
"""Test 35: Edit modal is pre-populated with all existing habit data"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check that form fields are pre-populated
|
|
assert "getElementById('habitName').value = habit.name" in content or \
|
|
"getElementById('habitName').value=habit.name" in content, \
|
|
"Should pre-populate habit name"
|
|
assert "getElementById('habitCategory').value = habit.category" in content or \
|
|
"getElementById('habitCategory').value=habit.category" in content, \
|
|
"Should pre-populate category"
|
|
assert "getElementById('habitPriority').value = habit.priority" in content or \
|
|
"getElementById('habitPriority').value=habit.priority" in content, \
|
|
"Should pre-populate priority"
|
|
assert "getElementById('habitNotes').value = habit.notes" in content or \
|
|
"getElementById('habitNotes').value=habit.notes" in content, \
|
|
"Should pre-populate notes"
|
|
assert "getElementById('frequencyType').value = habit.frequency" in content or \
|
|
"getElementById('frequencyType').value=habit.frequency" in content, \
|
|
"Should pre-populate frequency type"
|
|
|
|
# Check color and icon selection
|
|
assert 'selectedColor = habit.color' in content or 'selectedColor=habit.color' in content, \
|
|
"Should set selectedColor from habit"
|
|
assert 'selectedIcon = habit.icon' in content or 'selectedIcon=habit.icon' in content, \
|
|
"Should set selectedIcon from habit"
|
|
|
|
print("✓ Test 35: Edit modal pre-populated with habit data")
|
|
|
|
def test_edit_modal_title_and_button():
|
|
"""Test 36: Modal title shows 'Edit Habit' and button shows 'Save Changes'"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check that modal title is changed to Edit Habit
|
|
assert "modalTitle.textContent = 'Edit Habit'" in content or \
|
|
'modalTitle.textContent="Edit Habit"' in content or \
|
|
"modalTitle.textContent='Edit Habit'" in content, \
|
|
"Should set modal title to 'Edit Habit'"
|
|
|
|
# Check that submit button text is changed
|
|
assert "submitBtnText.textContent = 'Save Changes'" in content or \
|
|
'submitBtnText.textContent="Save Changes"' in content or \
|
|
"submitBtnText.textContent='Save Changes'" in content, \
|
|
"Should set button text to 'Save Changes'"
|
|
|
|
print("✓ Test 36: Modal title shows 'Edit Habit' and button shows 'Save Changes'")
|
|
|
|
def test_edit_modal_frequency_params():
|
|
"""Test 37: Frequency params display correctly for habit's current frequency type"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check that updateFrequencyParams is called
|
|
assert 'updateFrequencyParams()' in content, "Should call updateFrequencyParams()"
|
|
|
|
# Check that frequency params are pre-populated for specific types
|
|
assert 'specific_days' in content and 'habit.frequency.days' in content, \
|
|
"Should handle specific_days frequency params"
|
|
assert 'x_per_week' in content and 'habit.frequency.count' in content, \
|
|
"Should handle x_per_week frequency params"
|
|
assert 'custom' in content and 'habit.frequency.interval' in content, \
|
|
"Should handle custom frequency params"
|
|
|
|
# Check that day checkboxes are pre-populated
|
|
assert 'cb.checked = habit.frequency.days.includes' in content or \
|
|
'cb.checked=habit.frequency.days.includes' in content, \
|
|
"Should pre-select days for specific_days frequency"
|
|
|
|
print("✓ Test 37: Frequency params display correctly for current frequency")
|
|
|
|
def test_edit_modal_icon_color_pickers():
|
|
"""Test 38: Icon and color pickers show current selections"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check that pickers are initialized after setting values
|
|
assert 'initColorPicker()' in content, "Should call initColorPicker()"
|
|
assert 'initIconPicker()' in content, "Should call initIconPicker()"
|
|
|
|
# Check that selectedColor and selectedIcon are set before initialization
|
|
showEditIndex = content.find('function showEditHabitModal')
|
|
initColorIndex = content.find('initColorPicker()', showEditIndex)
|
|
selectedColorIndex = content.find('selectedColor = habit.color', showEditIndex)
|
|
|
|
assert selectedColorIndex > 0 and selectedColorIndex < initColorIndex, \
|
|
"Should set selectedColor before calling initColorPicker()"
|
|
|
|
print("✓ Test 38: Icon and color pickers show current selections")
|
|
|
|
def test_edit_modal_submit_put():
|
|
"""Test 39: Submit sends PUT /echo/api/habits/{id} and refreshes list on success"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check that editingHabitId is tracked
|
|
assert 'let editingHabitId' in content or 'editingHabitId' in content, \
|
|
"Should track editingHabitId"
|
|
|
|
# Check that isEditing is determined
|
|
assert 'const isEditing = editingHabitId !== null' in content or \
|
|
'const isEditing=editingHabitId!==null' in content or \
|
|
'isEditing = editingHabitId !== null' in content, \
|
|
"Should determine if editing"
|
|
|
|
# Check that URL and method are conditional
|
|
assert "const url = isEditing ? `/echo/api/habits/${editingHabitId}` : '/echo/api/habits'" in content or \
|
|
'const url=isEditing?`/echo/api/habits/${editingHabitId}`' in content or \
|
|
"url = isEditing ? `/echo/api/habits/${editingHabitId}` : '/echo/api/habits'" in content, \
|
|
"URL should be conditional based on isEditing"
|
|
|
|
assert "const method = isEditing ? 'PUT' : 'POST'" in content or \
|
|
"const method=isEditing?'PUT':'POST'" in content or \
|
|
"method = isEditing ? 'PUT' : 'POST'" in content, \
|
|
"Method should be conditional (PUT for edit, POST for create)"
|
|
|
|
# Check that loadHabits is called after success
|
|
assert 'await loadHabits()' in content, "Should refresh habit list after success"
|
|
|
|
print("✓ Test 39: Submit sends PUT and refreshes list")
|
|
|
|
def test_edit_modal_toast_messages():
|
|
"""Test 40: Toast shown for success and error"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check for conditional success message
|
|
assert "isEditing ? 'Habit updated!' : 'Habit created successfully!'" in content or \
|
|
"isEditing?'Habit updated!':'Habit created successfully!'" in content, \
|
|
"Should show different toast message for edit vs create"
|
|
|
|
# Check that error toast handles both edit and create
|
|
assert 'Failed to ${isEditing' in content or 'Failed to ' + '${isEditing' in content, \
|
|
"Error toast should be conditional"
|
|
|
|
print("✓ Test 40: Toast messages for success and error")
|
|
|
|
def test_edit_modal_add_resets_state():
|
|
"""Test 41: showAddHabitModal resets editingHabitId and modal UI"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Find showAddHabitModal function
|
|
add_modal_start = content.find('function showAddHabitModal()')
|
|
add_modal_end = content.find('function ', add_modal_start + 1)
|
|
add_modal_func = content[add_modal_start:add_modal_end]
|
|
|
|
# Check that editingHabitId is reset
|
|
assert 'editingHabitId = null' in add_modal_func or 'editingHabitId=null' in add_modal_func, \
|
|
"showAddHabitModal should reset editingHabitId to null"
|
|
|
|
# Check that modal title is reset to 'Add Habit'
|
|
assert "modalTitle.textContent = 'Add Habit'" in add_modal_func or \
|
|
'modalTitle.textContent="Add Habit"' in add_modal_func, \
|
|
"Should reset modal title to 'Add Habit'"
|
|
|
|
# Check that button text is reset to 'Create Habit'
|
|
assert "submitBtnText.textContent = 'Create Habit'" in add_modal_func or \
|
|
'submitBtnText.textContent="Create Habit"' in add_modal_func, \
|
|
"Should reset button text to 'Create Habit'"
|
|
|
|
print("✓ Test 41: showAddHabitModal resets editing state")
|
|
|
|
def test_edit_modal_close_resets_state():
|
|
"""Test 42: closeHabitModal resets editingHabitId"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Find closeHabitModal function
|
|
close_modal_start = content.find('function closeHabitModal()')
|
|
close_modal_end = content.find('function ', close_modal_start + 1)
|
|
close_modal_func = content[close_modal_start:close_modal_end]
|
|
|
|
# Check that editingHabitId is reset when closing
|
|
assert 'editingHabitId = null' in close_modal_func or 'editingHabitId=null' in close_modal_func, \
|
|
"closeHabitModal should reset editingHabitId to null"
|
|
|
|
print("✓ Test 42: closeHabitModal resets editing state")
|
|
|
|
def test_edit_modal_no_console_errors():
|
|
"""Test 43: No obvious console error sources in edit modal code"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check for common error patterns
|
|
assert content.count('getElementById(') > 0, "Should use getElementById"
|
|
|
|
# Check that habit is validated before use
|
|
showEditIndex = content.find('function showEditHabitModal')
|
|
showEditEnd = content.find('\n }', showEditIndex + 500) # Find end of function
|
|
showEditFunc = content[showEditIndex:showEditEnd]
|
|
|
|
assert 'if (!habit)' in showEditFunc or 'if(!habit)' in showEditFunc, \
|
|
"Should check if habit exists before using it"
|
|
assert 'showToast' in showEditFunc and 'error' in showEditFunc, \
|
|
"Should show error toast if habit not found"
|
|
|
|
print("✓ Test 43: No obvious console error sources")
|
|
|
|
def test_typecheck_us009():
|
|
"""Test 44: Typecheck passes - all edit modal functions and variables defined"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check that editingHabitId is declared
|
|
assert 'let editingHabitId' in content, "editingHabitId should be declared"
|
|
|
|
# Check that showEditHabitModal is fully implemented (not placeholder)
|
|
assert 'function showEditHabitModal(habitId)' in content, "showEditHabitModal should be defined"
|
|
assert 'alert' not in content[content.find('function showEditHabitModal'):content.find('function showEditHabitModal')+1000], \
|
|
"showEditHabitModal should not be a placeholder with alert()"
|
|
|
|
# Check that submitHabitForm handles both create and edit
|
|
assert 'const isEditing' in content or 'isEditing' in content, \
|
|
"submitHabitForm should determine if editing"
|
|
|
|
print("✓ Test 44: Typecheck passes (edit modal fully implemented)")
|
|
|
|
def test_checkin_simple_click():
|
|
"""Test 45: Simple click on check-in button sends POST request"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check that checkInHabit function exists and does POST
|
|
assert 'function checkInHabit' in content or 'async function checkInHabit' in content, \
|
|
"checkInHabit function should be defined"
|
|
|
|
checkin_start = content.find('function checkInHabit')
|
|
checkin_end = content.find('\n }', checkin_start + 500)
|
|
checkin_func = content[checkin_start:checkin_end]
|
|
|
|
assert "fetch(`/echo/api/habits/${habitId}/check`" in checkin_func or \
|
|
'fetch(`/echo/api/habits/${habitId}/check`' in checkin_func, \
|
|
"Should POST to /echo/api/habits/{id}/check"
|
|
assert "method: 'POST'" in checkin_func, "Should use POST method"
|
|
|
|
print("✓ Test 45: Simple click sends POST to check-in endpoint")
|
|
|
|
def test_checkin_detail_modal_structure():
|
|
"""Test 46: Check-in detail modal exists with note, rating, and mood fields"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check modal exists
|
|
assert 'id="checkinModal"' in content, "Should have check-in detail modal"
|
|
assert 'Check-In Details' in content or 'Check-in Details' in content, \
|
|
"Modal should have title 'Check-In Details'"
|
|
|
|
# Check for note textarea
|
|
assert 'id="checkinNote"' in content, "Should have note textarea"
|
|
assert '<textarea' in content, "Should have textarea element"
|
|
|
|
# Check for rating stars
|
|
assert 'rating-star' in content, "Should have rating star elements"
|
|
assert 'selectRating' in content, "Should have selectRating function"
|
|
|
|
# Check for mood buttons
|
|
assert 'mood-btn' in content, "Should have mood button elements"
|
|
assert 'selectMood' in content, "Should have selectMood function"
|
|
assert '😊' in content and '😐' in content and '😞' in content, \
|
|
"Should have happy, neutral, and sad emojis"
|
|
|
|
print("✓ Test 46: Check-in detail modal has note, rating, and mood fields")
|
|
|
|
def test_checkin_long_press_handler():
|
|
"""Test 47: Long-press (mobile) and right-click (desktop) handlers exist"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check for long-press handling functions
|
|
assert 'handleCheckInButtonPress' in content, \
|
|
"Should have handleCheckInButtonPress function"
|
|
assert 'handleCheckInButtonRelease' in content, \
|
|
"Should have handleCheckInButtonRelease function"
|
|
|
|
# Check for contextmenu event (right-click)
|
|
assert "contextmenu" in content, "Should handle contextmenu event for right-click"
|
|
|
|
# Check for touch/mouse events
|
|
assert "mousedown" in content, "Should handle mousedown event"
|
|
assert "mouseup" in content, "Should handle mouseup event"
|
|
assert "touchstart" in content or "touch" in content, \
|
|
"Should handle touch events for mobile"
|
|
|
|
# Check for long-press timer
|
|
assert 'longPressTimer' in content, "Should track long-press timer"
|
|
assert '500' in content, "Should use 500ms delay for long-press"
|
|
|
|
print("✓ Test 47: Long-press and right-click handlers exist")
|
|
|
|
def test_checkin_detail_modal_functions():
|
|
"""Test 48: Modal functions (show, close, selectRating, selectMood) are defined"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check modal control functions
|
|
assert 'function showCheckInDetailModal' in content, \
|
|
"Should have showCheckInDetailModal function"
|
|
assert 'function closeCheckinModal' in content, \
|
|
"Should have closeCheckinModal function"
|
|
|
|
# Check selection functions
|
|
assert 'function selectRating' in content, "Should have selectRating function"
|
|
assert 'function selectMood' in content, "Should have selectMood function"
|
|
|
|
# Check submit function
|
|
assert 'function submitCheckInDetail' in content or 'async function submitCheckInDetail' in content, \
|
|
"Should have submitCheckInDetail function"
|
|
|
|
print("✓ Test 48: Check-in modal functions are defined")
|
|
|
|
def test_checkin_detail_submit():
|
|
"""Test 49: Detail modal submit sends note, rating, and mood"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Find submitCheckInDetail function
|
|
submit_start = content.find('function submitCheckInDetail')
|
|
submit_end = content.find('\n }', submit_start + 1000)
|
|
submit_func = content[submit_start:submit_end]
|
|
|
|
# Check it builds body with optional fields
|
|
assert 'body.note' in submit_func or 'if (note)' in submit_func, \
|
|
"Should add note to body if provided"
|
|
assert 'body.rating' in submit_func or 'checkInRating' in submit_func, \
|
|
"Should add rating to body if provided"
|
|
assert 'body.mood' in submit_func or 'checkInMood' in submit_func, \
|
|
"Should add mood to body if provided"
|
|
|
|
# Check it POSTs to API
|
|
assert "fetch(`/echo/api/habits/" in submit_func, "Should POST to API"
|
|
assert "/check`" in submit_func, "Should POST to check endpoint"
|
|
|
|
print("✓ Test 49: Detail modal submit includes optional fields")
|
|
|
|
def test_checkin_toast_with_streak():
|
|
"""Test 50: Toast notification shows 'Habit checked! 🔥 Streak: X'"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check for toast with streak message
|
|
assert 'Habit checked!' in content, "Should show 'Habit checked!' message"
|
|
assert '🔥' in content, "Should include fire emoji in streak message"
|
|
assert 'Streak:' in content, "Should show streak count"
|
|
|
|
# Check that streak value comes from API response
|
|
checkin_start = content.find('function checkInHabit')
|
|
checkin_area = content[checkin_start:checkin_start+5000] # Increased for toggle implementation
|
|
|
|
assert 'updatedHabit' in checkin_area or 'await response.json()' in checkin_area, \
|
|
"Should get updated habit from response"
|
|
assert 'streak' in checkin_area, "Should access streak from response"
|
|
|
|
print("✓ Test 50: Toast shows 'Habit checked! 🔥 Streak: X'")
|
|
|
|
def test_checkin_button_disabled_after():
|
|
"""Test 51: Check-in button shows checked state after check-in (toggle design)"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check that renderHabitCard uses isCheckedToday to determine state
|
|
assert 'isCheckedToday(habit)' in content, \
|
|
"Should check if habit is checked today"
|
|
|
|
# Check button uses 'checked' class instead of disabled (US-003 toggle behavior)
|
|
assert 'isDoneToday ? \'checked\' : \'\'' in content or \
|
|
'${isDoneToday ? \'checked\' : \'\'}' in content, \
|
|
"Button should use 'checked' class (not disabled)"
|
|
|
|
# Check button displays checkmark when done (compact design)
|
|
assert 'isDoneToday ? \'✓\' : \'○\'' in content or \
|
|
'${isDoneToday ? \'✓\' : \'○\'}' in content, \
|
|
"Button should show ✓ when checked, ○ when unchecked (compact)"
|
|
|
|
print("✓ Test 51: Button shows checked state after check-in (toggle design)")
|
|
|
|
def test_checkin_pulse_animation():
|
|
"""Test 52: Pulse animation plays on card after successful check-in"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check for pulse animation CSS
|
|
assert '@keyframes pulse' in content, "Should define pulse animation"
|
|
assert 'transform: scale' in content, "Pulse should use scale transform"
|
|
assert '.pulse' in content, "Should have pulse CSS class"
|
|
|
|
# Check that checkInHabit adds pulse class
|
|
checkin_start = content.find('function checkInHabit')
|
|
checkin_area = content[checkin_start:checkin_start+5000] # Increased for toggle implementation
|
|
|
|
assert 'pulse' in checkin_area or 'classList.add' in checkin_area, \
|
|
"Should add pulse class to card after check-in"
|
|
|
|
print("✓ Test 52: Pulse animation defined and applied")
|
|
|
|
def test_checkin_prevent_context_menu():
|
|
"""Test 53: Right-click prevents default context menu"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check for preventDefault on contextmenu
|
|
press_handler_start = content.find('function handleCheckInButtonPress')
|
|
press_handler_end = content.find('\n }', press_handler_start + 1000)
|
|
press_handler = content[press_handler_start:press_handler_end]
|
|
|
|
assert 'contextmenu' in press_handler, "Should check for contextmenu event"
|
|
assert 'preventDefault()' in press_handler, "Should call preventDefault on right-click"
|
|
|
|
print("✓ Test 53: Right-click prevents default context menu")
|
|
|
|
def test_checkin_event_listeners_attached():
|
|
"""Test 54: Event listeners are attached to check-in buttons in renderHabits"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Find renderHabits function
|
|
render_start = content.find('function renderHabits()')
|
|
render_end = content.find('\n }', render_start + 2000)
|
|
render_func = content[render_start:render_end]
|
|
|
|
# Check that event listeners are attached
|
|
assert 'addEventListener' in render_func, \
|
|
"Should attach event listeners after rendering"
|
|
assert 'contextmenu' in render_func or 'contextmenu' in content[render_end:render_end+1000], \
|
|
"Should attach contextmenu listener"
|
|
assert 'mousedown' in render_func or 'mousedown' in content[render_end:render_end+1000], \
|
|
"Should attach mousedown listener"
|
|
|
|
# Check that we iterate through habits to attach listeners
|
|
assert 'habits.forEach' in render_func or 'for' in render_func, \
|
|
"Should iterate through habits to attach listeners"
|
|
|
|
print("✓ Test 54: Event listeners attached in renderHabits")
|
|
|
|
def test_typecheck_us010():
|
|
"""Test 55: Typecheck passes - all check-in functions defined"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check all required functions are defined
|
|
required_functions = [
|
|
'checkInHabit',
|
|
'showCheckInDetailModal',
|
|
'closeCheckinModal',
|
|
'selectRating',
|
|
'selectMood',
|
|
'submitCheckInDetail',
|
|
'handleCheckInButtonPress',
|
|
'handleCheckInButtonRelease',
|
|
'handleCheckInButtonCancel'
|
|
]
|
|
|
|
for func in required_functions:
|
|
assert f'function {func}' in content or f'async function {func}' in content, \
|
|
f"{func} should be defined"
|
|
|
|
# Check state variables
|
|
assert 'let checkInHabitId' in content, "checkInHabitId should be declared"
|
|
assert 'let checkInRating' in content, "checkInRating should be declared"
|
|
assert 'let checkInMood' in content, "checkInMood should be declared"
|
|
assert 'let longPressTimer' in content, "longPressTimer should be declared"
|
|
|
|
print("✓ Test 55: Typecheck passes (all check-in functions and variables defined)")
|
|
|
|
# ========== US-011 Tests: Skip, lives display, and delete confirmation ==========
|
|
|
|
def test_skip_button_visible():
|
|
"""Test 56: Skip functionality exists (hidden in compact card view)"""
|
|
html_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = html_path.read_text()
|
|
|
|
# Skip button is hidden in compact view, but skipHabitDay function still exists
|
|
assert 'async function skipHabitDay' in content or 'function skipHabitDay' in content, \
|
|
"skipHabitDay function should exist for skip functionality"
|
|
|
|
# In compact design, skip button and lives are NOT visible on card
|
|
# This is correct behavior for US-002 compact cards
|
|
|
|
print("✓ Test 56: Skip functionality exists (hidden in compact card view)")
|
|
|
|
def test_skip_confirmation_dialog():
|
|
"""Test 57: Clicking skip shows confirmation dialog"""
|
|
html_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = html_path.read_text()
|
|
|
|
# Check skipHabitDay function exists
|
|
assert 'async function skipHabitDay' in content, "skipHabitDay function should exist"
|
|
|
|
# Check confirmation dialog
|
|
assert 'Use 1 life to skip today?' in content, "Skip confirmation dialog should ask 'Use 1 life to skip today?'"
|
|
assert 'confirm(' in content, "Should use confirm dialog"
|
|
|
|
print("✓ Test 57: Clicking skip shows confirmation dialog")
|
|
|
|
def test_skip_sends_post_and_refreshes():
|
|
"""Test 58: Confirming skip sends POST /echo/api/habits/{id}/skip and refreshes card"""
|
|
html_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = html_path.read_text()
|
|
|
|
# Check API endpoint
|
|
assert "/echo/api/habits/${habitId}/skip" in content, "Should POST to /echo/api/habits/{id}/skip"
|
|
assert "method: 'POST'" in content, "Should use POST method"
|
|
|
|
# Check refresh
|
|
assert 'await loadHabits()' in content, "Should refresh habits list after skip"
|
|
|
|
print("✓ Test 58: Confirming skip sends POST /echo/api/habits/{id}/skip and refreshes card")
|
|
|
|
def test_skip_button_disabled_when_no_lives():
|
|
"""Test 59: Skip logic handles lives correctly (compact view hides UI)"""
|
|
html_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = html_path.read_text()
|
|
|
|
# Skip functionality exists even though UI is hidden in compact view
|
|
# The logic for handling lives still needs to exist
|
|
assert 'skipHabitDay' in content, "Skip function should exist"
|
|
|
|
# In compact design, skip button is hidden so this test just verifies function exists
|
|
|
|
print("✓ Test 59: Skip logic exists (UI hidden in compact view)")
|
|
|
|
def test_skip_toast_message():
|
|
"""Test 60: Toast shows 'Day skipped. X lives remaining.' after skip"""
|
|
html_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = html_path.read_text()
|
|
|
|
# Check toast message format
|
|
assert 'Day skipped' in content, "Toast should include 'Day skipped'"
|
|
assert 'remaining' in content, "Toast should show remaining lives"
|
|
|
|
# Check life/lives plural handling
|
|
assert "remainingLives === 1 ? 'life' : 'lives'" in content, "Should handle singular/plural for lives"
|
|
|
|
print("✓ Test 60: Toast shows 'Day skipped. X lives remaining.' after skip")
|
|
|
|
def test_delete_confirmation_with_habit_name():
|
|
"""Test 61: Clicking trash icon shows delete confirmation dialog with habit name"""
|
|
html_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = html_path.read_text()
|
|
|
|
# Check delete function exists
|
|
assert 'async function deleteHabit' in content, "deleteHabit function should exist"
|
|
|
|
# Check confirmation uses habit name
|
|
assert 'Delete ${habitName}?' in content or 'Delete ' in content, "Delete confirmation should include habit name"
|
|
assert 'This cannot be undone' in content, "Delete confirmation should warn 'This cannot be undone'"
|
|
|
|
print("✓ Test 61: Clicking trash icon shows delete confirmation dialog with habit name")
|
|
|
|
def test_delete_sends_delete_request():
|
|
"""Test 62: Confirming delete sends DELETE /echo/api/habits/{id} and removes card"""
|
|
html_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = html_path.read_text()
|
|
|
|
# Check DELETE request
|
|
assert "method: 'DELETE'" in content, "Should use DELETE method"
|
|
assert "/echo/api/habits/${habitId}" in content or "/echo/api/habits/" in content, "Should DELETE to /echo/api/habits/{id}"
|
|
|
|
# Check refresh (removes card from DOM via re-render)
|
|
assert 'await loadHabits()' in content, "Should refresh habits list after delete"
|
|
|
|
print("✓ Test 62: Confirming delete sends DELETE /echo/api/habits/{id} and removes card")
|
|
|
|
def test_delete_toast_message():
|
|
"""Test 63: Toast shows 'Habit deleted' after deletion"""
|
|
html_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = html_path.read_text()
|
|
|
|
# Check success toast
|
|
assert 'Habit deleted' in content, "Toast should say 'Habit deleted'"
|
|
|
|
print("✓ Test 63: Toast shows 'Habit deleted' after deletion")
|
|
|
|
def test_skip_delete_no_console_errors():
|
|
"""Test 64: No obvious console error sources in skip and delete code"""
|
|
html_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = html_path.read_text()
|
|
|
|
# Check error handling in skipHabitDay
|
|
skip_func = content[content.find('async function skipHabitDay'):content.find('async function skipHabitDay') + 1500]
|
|
assert 'try' in skip_func and 'catch' in skip_func, "skipHabitDay should have try/catch"
|
|
assert 'console.error' in skip_func, "skipHabitDay should log errors"
|
|
|
|
# Check error handling in deleteHabit
|
|
delete_func = content[content.find('async function deleteHabit'):content.find('async function deleteHabit') + 1500]
|
|
assert 'try' in delete_func and 'catch' in delete_func, "deleteHabit should have try/catch"
|
|
assert 'console.error' in delete_func, "deleteHabit should log errors"
|
|
|
|
print("✓ Test 64: No obvious console error sources in skip and delete code")
|
|
|
|
def test_typecheck_us011():
|
|
"""Test 65: Typecheck passes (all skip and delete functions defined)"""
|
|
html_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = html_path.read_text()
|
|
|
|
# Check function definitions
|
|
functions = [
|
|
'skipHabitDay',
|
|
'deleteHabit',
|
|
]
|
|
|
|
for func in functions:
|
|
assert f"function {func}" in content, \
|
|
f"{func} should be defined"
|
|
|
|
# Check that habits are cached for delete
|
|
assert 'localStorage.setItem' in content, "Should cache habits in localStorage"
|
|
assert 'habitsCache' in content, "Should use 'habitsCache' key"
|
|
|
|
print("✓ Test 65: Typecheck passes (all skip and delete functions defined)")
|
|
|
|
### US-012: Filter and sort controls ###
|
|
|
|
def test_filter_bar_exists():
|
|
"""Test 66: Filter bar with category, status, and sort dropdowns appears above habit grid"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'class="filter-bar"' in content, "Should have filter-bar element"
|
|
assert 'id="categoryFilter"' in content, "Should have category filter dropdown"
|
|
assert 'id="statusFilter"' in content, "Should have status filter dropdown"
|
|
assert 'id="sortSelect"' in content, "Should have sort dropdown"
|
|
print("✓ Test 66: Filter bar with dropdowns exists")
|
|
|
|
def test_category_filter_options():
|
|
"""Test 67: Category filter has All, Work, Health, Growth, Personal options"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check for category options
|
|
assert 'value="all">All</option>' in content, "Should have 'All' option"
|
|
assert 'value="work">Work</option>' in content, "Should have 'Work' option"
|
|
assert 'value="health">Health</option>' in content, "Should have 'Health' option"
|
|
assert 'value="growth">Growth</option>' in content, "Should have 'Growth' option"
|
|
assert 'value="personal">Personal</option>' in content, "Should have 'Personal' option"
|
|
print("✓ Test 67: Category filter has correct options")
|
|
|
|
def test_status_filter_options():
|
|
"""Test 68: Status filter has All, Active Today, Done Today, Overdue options"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'value="all">All</option>' in content, "Should have 'All' option"
|
|
assert 'value="active_today">Active Today</option>' in content, "Should have 'Active Today' option"
|
|
assert 'value="done_today">Done Today</option>' in content, "Should have 'Done Today' option"
|
|
assert 'value="overdue">Overdue</option>' in content, "Should have 'Overdue' option"
|
|
print("✓ Test 68: Status filter has correct options")
|
|
|
|
def test_sort_dropdown_options():
|
|
"""Test 69: Sort dropdown has Priority, Name, and Streak options (asc/desc)"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'value="priority_asc"' in content, "Should have 'Priority (Low to High)' option"
|
|
assert 'value="priority_desc"' in content, "Should have 'Priority (High to Low)' option"
|
|
assert 'value="name_asc"' in content, "Should have 'Name A-Z' option"
|
|
assert 'value="name_desc"' in content, "Should have 'Name Z-A' option"
|
|
assert 'value="streak_desc"' in content, "Should have 'Streak (Highest)' option"
|
|
assert 'value="streak_asc"' in content, "Should have 'Streak (Lowest)' option"
|
|
print("✓ Test 69: Sort dropdown has correct options")
|
|
|
|
def test_filter_functions_exist():
|
|
"""Test 70: applyFiltersAndSort, filterHabits, and sortHabits functions are defined"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'function applyFiltersAndSort()' in content, "Should have applyFiltersAndSort function"
|
|
assert 'function filterHabits(' in content, "Should have filterHabits function"
|
|
assert 'function sortHabits(' in content, "Should have sortHabits function"
|
|
assert 'function restoreFilters()' in content, "Should have restoreFilters function"
|
|
print("✓ Test 70: Filter and sort functions are defined")
|
|
|
|
def test_filter_calls_on_change():
|
|
"""Test 71: Filter dropdowns call applyFiltersAndSort on change"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'onchange="applyFiltersAndSort()"' in content, "Filters should call applyFiltersAndSort on change"
|
|
|
|
# Count how many times onchange appears (should be 3: category, status, sort)
|
|
count = content.count('onchange="applyFiltersAndSort()"')
|
|
assert count >= 3, f"Should have at least 3 onchange handlers, found {count}"
|
|
print("✓ Test 71: Filter dropdowns call applyFiltersAndSort on change")
|
|
|
|
def test_localstorage_persistence():
|
|
"""Test 72: Filter selections are saved to localStorage"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert "localStorage.setItem('habitCategoryFilter'" in content, "Should save category filter to localStorage"
|
|
assert "localStorage.setItem('habitStatusFilter'" in content, "Should save status filter to localStorage"
|
|
assert "localStorage.setItem('habitSort'" in content, "Should save sort to localStorage"
|
|
print("✓ Test 72: Filter selections saved to localStorage")
|
|
|
|
def test_localstorage_restore():
|
|
"""Test 73: Filter selections are restored from localStorage on page load"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert "localStorage.getItem('habitCategoryFilter')" in content, "Should restore category filter from localStorage"
|
|
assert "localStorage.getItem('habitStatusFilter')" in content, "Should restore status filter from localStorage"
|
|
assert "localStorage.getItem('habitSort')" in content, "Should restore sort from localStorage"
|
|
assert 'restoreFilters()' in content, "Should call restoreFilters on page load"
|
|
print("✓ Test 73: Filter selections restored from localStorage")
|
|
|
|
def test_filter_logic_implementation():
|
|
"""Test 74: filterHabits function checks category and status correctly"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check category filter logic
|
|
assert "categoryFilter !== 'all'" in content, "Should check if category filter is not 'all'"
|
|
assert "habit.category" in content, "Should compare habit.category"
|
|
|
|
# Check status filter logic
|
|
assert "statusFilter !== 'all'" in content, "Should check if status filter is not 'all'"
|
|
assert "should_check_today" in content or "shouldCheckToday" in content, "Should use should_check_today for status filtering"
|
|
print("✓ Test 74: Filter logic checks category and status")
|
|
|
|
def test_sort_logic_implementation():
|
|
"""Test 75: sortHabits function handles all sort options"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check that sort function handles all options
|
|
assert "'priority_asc'" in content, "Should handle priority_asc"
|
|
assert "'priority_desc'" in content, "Should handle priority_desc"
|
|
assert "'name_asc'" in content, "Should handle name_asc"
|
|
assert "'name_desc'" in content, "Should handle name_desc"
|
|
assert "'streak_desc'" in content, "Should handle streak_desc"
|
|
assert "'streak_asc'" in content, "Should handle streak_asc"
|
|
assert 'localeCompare' in content, "Should use localeCompare for name sorting"
|
|
print("✓ Test 75: Sort logic handles all options")
|
|
|
|
def test_backend_provides_should_check_today():
|
|
"""Test 76: Backend API enriches habits with should_check_today field"""
|
|
api_path = Path(__file__).parent.parent / 'api.py'
|
|
content = api_path.read_text()
|
|
|
|
# Check that should_check_today is added in handle_habits_get
|
|
assert "should_check_today" in content, "Backend should add should_check_today field"
|
|
assert "habits_helpers.should_check_today" in content, "Should use should_check_today helper"
|
|
print("✓ Test 76: Backend provides should_check_today field")
|
|
|
|
def test_typecheck_us012():
|
|
"""Test 77: Typecheck passes for api.py"""
|
|
api_path = Path(__file__).parent.parent / 'api.py'
|
|
result = os.system(f'python3 -m py_compile {api_path} 2>/dev/null')
|
|
assert result == 0, "api.py should pass typecheck (syntax check)"
|
|
print("✓ Test 77: Typecheck passes")
|
|
|
|
# ============================================================================
|
|
# US-013: Frontend - Stats section and weekly summary
|
|
# ============================================================================
|
|
|
|
def test_stats_section_exists():
|
|
"""Test 78: Stats section exists with 4 metric cards"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'id="statsSection"' in content, "Should have statsSection element"
|
|
assert 'class="stats-row"' in content, "Should have stats-row container"
|
|
assert 'class="stat-card"' in content, "Should have stat-card elements"
|
|
|
|
# Check for the 4 metrics
|
|
assert 'id="statTotalHabits"' in content, "Should have Total Habits metric"
|
|
assert 'id="statAvgCompletion"' in content, "Should have Avg Completion metric"
|
|
assert 'id="statBestStreak"' in content, "Should have Best Streak metric"
|
|
assert 'id="statTotalLives"' in content, "Should have Total Lives metric"
|
|
|
|
print("✓ Test 78: Stats section with 4 metric cards exists")
|
|
|
|
def test_stats_labels_correct():
|
|
"""Test 79: Stat cards have correct labels"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'Total Habits' in content, "Should have 'Total Habits' label"
|
|
assert 'Avg Completion (30d)' in content or 'Avg Completion' in content, \
|
|
"Should have 'Avg Completion' label"
|
|
assert 'Best Streak' in content, "Should have 'Best Streak' label"
|
|
assert 'Total Lives' in content, "Should have 'Total Lives' label"
|
|
|
|
print("✓ Test 79: Stat cards have correct labels")
|
|
|
|
def test_weekly_summary_exists():
|
|
"""Test 80: Weekly summary section exists and is collapsible"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'class="weekly-summary"' in content, "Should have weekly-summary section"
|
|
assert 'class="weekly-summary-header"' in content, "Should have clickable header"
|
|
assert 'Weekly Summary' in content, "Should have 'Weekly Summary' title"
|
|
assert 'toggleWeeklySummary()' in content, "Should have toggle function"
|
|
assert 'id="weeklySummaryContent"' in content, "Should have collapsible content container"
|
|
assert 'id="weeklySummaryChevron"' in content, "Should have chevron icon"
|
|
|
|
print("✓ Test 80: Weekly summary section exists and is collapsible")
|
|
|
|
def test_weekly_chart_structure():
|
|
"""Test 81: Weekly chart displays bars for Mon-Sun"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'id="weeklyChart"' in content, "Should have weeklyChart container"
|
|
assert 'class="weekly-chart"' in content, "Should have weekly-chart class"
|
|
|
|
# Check for bar rendering in JS
|
|
assert 'weekly-bar' in content, "Should render weekly bars in CSS/JS"
|
|
assert 'weekly-day-label' in content, "Should have day labels"
|
|
|
|
print("✓ Test 81: Weekly chart structure exists")
|
|
|
|
def test_weekly_stats_text():
|
|
"""Test 82: Weekly stats show completed and skipped counts"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'id="weeklyCompletedText"' in content, "Should have completed count element"
|
|
assert 'id="weeklySkippedText"' in content, "Should have skipped count element"
|
|
assert 'completed this week' in content, "Should have 'completed this week' text"
|
|
assert 'skipped this week' in content, "Should have 'skipped this week' text"
|
|
|
|
print("✓ Test 82: Weekly stats text elements exist")
|
|
|
|
def test_stats_functions_exist():
|
|
"""Test 83: renderStats and renderWeeklySummary functions exist"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert 'function renderStats()' in content, "Should have renderStats function"
|
|
assert 'function renderWeeklySummary()' in content, "Should have renderWeeklySummary function"
|
|
assert 'function toggleWeeklySummary()' in content, "Should have toggleWeeklySummary function"
|
|
|
|
print("✓ Test 83: Stats rendering functions exist")
|
|
|
|
def test_stats_calculations():
|
|
"""Test 84: Stats calculations use client-side logic"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check for total habits calculation
|
|
assert 'totalHabits' in content, "Should calculate total habits"
|
|
|
|
# Check for avg completion calculation
|
|
assert 'avgCompletion' in content or 'completion_rate_30d' in content, \
|
|
"Should calculate average completion rate"
|
|
|
|
# Check for best streak calculation
|
|
assert 'bestStreak' in content or 'Math.max' in content, \
|
|
"Should calculate best streak across all habits"
|
|
|
|
# Check for total lives calculation
|
|
assert 'totalLives' in content or '.lives' in content, \
|
|
"Should calculate total lives"
|
|
|
|
print("✓ Test 84: Stats calculations implemented")
|
|
|
|
def test_weekly_chart_bars_proportional():
|
|
"""Test 85: Weekly chart bars are proportional to completion count"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check that bars use height proportional to count
|
|
assert 'height' in content and ('style' in content or 'height:' in content), \
|
|
"Should set bar height dynamically"
|
|
assert 'maxCompletions' in content or 'Math.max' in content, \
|
|
"Should calculate max for scaling"
|
|
|
|
print("✓ Test 85: Weekly chart bars are proportional")
|
|
|
|
def test_stats_called_from_render():
|
|
"""Test 86: renderStats is called when renderHabits is called"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Find renderHabits function
|
|
render_habits_start = content.find('function renderHabits()')
|
|
assert render_habits_start > 0, "renderHabits function should exist"
|
|
|
|
# Check that renderStats is called within renderHabits
|
|
render_habits_section = content[render_habits_start:render_habits_start + 2000]
|
|
assert 'renderStats()' in render_habits_section, \
|
|
"renderStats() should be called from renderHabits()"
|
|
|
|
print("✓ Test 86: renderStats called from renderHabits")
|
|
|
|
def test_stats_css_styling():
|
|
"""Test 87: Stats section has proper CSS styling"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
assert '.stats-section' in content, "Should have stats-section CSS"
|
|
assert '.stats-row' in content, "Should have stats-row CSS"
|
|
assert '.stat-card' in content, "Should have stat-card CSS"
|
|
assert '.weekly-summary' in content, "Should have weekly-summary CSS"
|
|
assert '.weekly-chart' in content, "Should have weekly-chart CSS"
|
|
assert '.weekly-bar' in content, "Should have weekly-bar CSS"
|
|
|
|
print("✓ Test 87: Stats CSS styling exists")
|
|
|
|
def test_stats_no_console_errors():
|
|
"""Test 88: No obvious console error sources in stats code"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check that functions are properly defined
|
|
assert 'function renderStats()' in content, "renderStats should be defined"
|
|
assert 'function renderWeeklySummary()' in content, "renderWeeklySummary should be defined"
|
|
assert 'function toggleWeeklySummary()' in content, "toggleWeeklySummary should be defined"
|
|
|
|
# Check DOM element IDs are referenced correctly
|
|
assert "getElementById('statsSection')" in content or \
|
|
'getElementById("statsSection")' in content, \
|
|
"Should reference statsSection element"
|
|
assert "getElementById('statTotalHabits')" in content or \
|
|
'getElementById("statTotalHabits")' in content, \
|
|
"Should reference statTotalHabits element"
|
|
|
|
print("✓ Test 88: No obvious console error sources")
|
|
|
|
def test_typecheck_us013():
|
|
"""Test 89: Typecheck passes for api.py"""
|
|
api_path = Path(__file__).parent.parent / 'api.py'
|
|
result = os.system(f'python3 -m py_compile {api_path} 2>/dev/null')
|
|
assert result == 0, "api.py should pass typecheck (syntax check)"
|
|
print("✓ Test 89: Typecheck passes")
|
|
|
|
def test_mobile_grid_responsive():
|
|
"""Test 90: Grid shows 1 column on screens below 768px"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check for mobile breakpoint
|
|
assert '@media (max-width: 768px)' in content, "Should have mobile breakpoint"
|
|
assert 'grid-template-columns: 1fr' in content, "Should use 1 column on mobile"
|
|
print("✓ Test 90: Grid is responsive for mobile (1 column)")
|
|
|
|
def test_tablet_grid_responsive():
|
|
"""Test 91: Grid shows 2 columns between 768px and 1200px"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check for tablet breakpoint
|
|
assert '@media (min-width: 769px) and (max-width: 1200px)' in content, "Should have tablet breakpoint"
|
|
assert 'repeat(2, 1fr)' in content, "Should use 2 columns on tablet"
|
|
print("✓ Test 91: Grid shows 2 columns on tablet screens")
|
|
|
|
def test_touch_targets_44px():
|
|
"""Test 92: All buttons and interactive elements have minimum 44px touch target"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check for minimum touch target sizes in mobile styles
|
|
assert 'min-width: 44px' in content, "Should have min-width 44px for touch targets"
|
|
assert 'min-height: 44px' in content, "Should have min-height 44px for touch targets"
|
|
# Compact check button uses 44px on mobile (not 48px)
|
|
assert 'habit-card-check-btn-compact' in content, "Compact check button should exist"
|
|
print("✓ Test 92: Touch targets meet 44px minimum on mobile")
|
|
|
|
def test_modals_scrollable_mobile():
|
|
"""Test 93: Modals are scrollable with max-height 90vh on small screens"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check modal has max-height and overflow
|
|
assert 'max-height: 90vh' in content, "Modal should have max-height 90vh"
|
|
assert 'overflow-y: auto' in content, "Modal should have overflow-y auto for scrolling"
|
|
print("✓ Test 93: Modals are scrollable on small screens")
|
|
|
|
def test_pickers_wrap_mobile():
|
|
"""Test 94: Icon and color pickers wrap properly on mobile"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check for mobile grid adjustments in @media query
|
|
mobile_section = content[content.find('@media (max-width: 768px)'):content.find('@media (max-width: 768px)') + 3000]
|
|
|
|
assert 'grid-template-columns: repeat(4, 1fr)' in mobile_section, "Pickers should use 4 columns on mobile"
|
|
print("✓ Test 94: Icon and color pickers wrap properly on mobile")
|
|
|
|
def test_filter_bar_stacks_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 options mobile styles
|
|
mobile_section = content[content.find('@media (max-width: 768px)'):content.find('@media (max-width: 768px)') + 3000]
|
|
|
|
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"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check for stats row mobile layout
|
|
mobile_section = content[content.find('@media (max-width: 768px)'):content.find('@media (max-width: 768px)') + 3000]
|
|
|
|
assert '.stats-row' in mobile_section, "Should have stats-row mobile styles"
|
|
assert 'grid-template-columns: repeat(2, 1fr)' in mobile_section, "Stats should use 2x2 grid on mobile"
|
|
print("✓ Test 96: Stats row shows 2x2 grid on mobile")
|
|
|
|
def test_swipe_nav_integration():
|
|
"""Test 97: swipe-nav.js is integrated for page navigation"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check swipe-nav.js is included
|
|
assert 'src="/echo/swipe-nav.js"' in content, "Should include swipe-nav.js"
|
|
|
|
# Check swipe-nav.js has habits.html in pages array
|
|
swipe_nav_path = Path(__file__).parent.parent / 'swipe-nav.js'
|
|
if swipe_nav_path.exists():
|
|
swipe_content = swipe_nav_path.read_text()
|
|
assert 'habits.html' in swipe_content, "swipe-nav.js should include habits.html in pages array"
|
|
|
|
print("✓ Test 97: swipe-nav.js is integrated")
|
|
|
|
def test_mobile_form_inputs():
|
|
"""Test 98: Form inputs are touch-friendly on mobile"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check for form input mobile styles
|
|
mobile_section = content[content.find('@media (max-width: 768px)'):content.find('@media (max-width: 768px)') + 3000]
|
|
|
|
assert '.form-input' in mobile_section or 'min-height: 44px' in mobile_section, "Form inputs should have mobile styles"
|
|
print("✓ Test 98: Form inputs are touch-friendly on mobile")
|
|
|
|
def test_mobile_no_console_errors():
|
|
"""Test 99: No obvious console error sources in mobile-specific code"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Basic sanity checks
|
|
assert '<style>' in content, "Should have style tags"
|
|
assert '@media' in content, "Should have media queries"
|
|
assert '</style>' in content, "Style tags should be closed"
|
|
print("✓ Test 99: No obvious console error sources")
|
|
|
|
def test_typecheck_us014():
|
|
"""Test 100: Typecheck passes after US-014 changes"""
|
|
api_path = Path(__file__).parent.parent / 'api.py'
|
|
result = os.system(f'python3 -m py_compile {api_path}')
|
|
assert result == 0, "api.py should pass typecheck"
|
|
print("✓ Test 100: Typecheck passes")
|
|
|
|
def test_mobile_day_checkboxes_wrap():
|
|
"""Test 101: Day checkboxes wrap properly on mobile"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check for day checkboxes mobile styles
|
|
mobile_section = content[content.find('@media (max-width: 768px)'):content.find('@media (max-width: 768px)') + 3000]
|
|
|
|
assert '.day-checkboxes' in mobile_section, "Should have day-checkboxes mobile styles"
|
|
print("✓ Test 101: Day checkboxes wrap properly on mobile")
|
|
|
|
def run_all_tests():
|
|
"""Run all tests in sequence"""
|
|
tests = [
|
|
# US-006 tests
|
|
test_habits_html_exists,
|
|
test_habits_html_structure,
|
|
test_page_has_header,
|
|
test_empty_state,
|
|
test_grid_container,
|
|
test_index_navigation_link,
|
|
test_page_fetches_habits,
|
|
test_habit_card_rendering,
|
|
test_no_console_errors_structure,
|
|
test_typecheck,
|
|
# US-007 tests
|
|
test_card_colored_border,
|
|
test_card_header_icons,
|
|
test_card_streak_display,
|
|
test_card_checkin_button,
|
|
test_card_checkin_disabled_when_done,
|
|
test_card_lives_display,
|
|
test_card_completion_rate,
|
|
test_card_footer_category_priority,
|
|
test_card_lucide_createicons,
|
|
test_card_common_css_variables,
|
|
test_typecheck_us007,
|
|
# US-008 tests
|
|
test_modal_opens_on_add_habit_click,
|
|
test_modal_closes_on_x_and_outside_click,
|
|
test_modal_has_all_form_fields,
|
|
test_color_picker_presets_and_custom,
|
|
test_icon_picker_grid,
|
|
test_frequency_params_conditional,
|
|
test_client_side_validation,
|
|
test_submit_posts_to_api,
|
|
test_loading_state_on_submit,
|
|
test_toast_notifications,
|
|
test_modal_no_console_errors,
|
|
test_typecheck_us008,
|
|
# US-009 tests
|
|
test_edit_modal_opens_on_gear_icon,
|
|
test_edit_modal_prepopulated,
|
|
test_edit_modal_title_and_button,
|
|
test_edit_modal_frequency_params,
|
|
test_edit_modal_icon_color_pickers,
|
|
test_edit_modal_submit_put,
|
|
test_edit_modal_toast_messages,
|
|
test_edit_modal_add_resets_state,
|
|
test_edit_modal_close_resets_state,
|
|
test_edit_modal_no_console_errors,
|
|
test_typecheck_us009,
|
|
# US-010 tests
|
|
test_checkin_simple_click,
|
|
test_checkin_detail_modal_structure,
|
|
test_checkin_long_press_handler,
|
|
test_checkin_detail_modal_functions,
|
|
test_checkin_detail_submit,
|
|
test_checkin_toast_with_streak,
|
|
test_checkin_button_disabled_after,
|
|
test_checkin_pulse_animation,
|
|
test_checkin_prevent_context_menu,
|
|
test_checkin_event_listeners_attached,
|
|
test_typecheck_us010,
|
|
# US-011 tests
|
|
test_skip_button_visible,
|
|
test_skip_confirmation_dialog,
|
|
test_skip_sends_post_and_refreshes,
|
|
test_skip_button_disabled_when_no_lives,
|
|
test_skip_toast_message,
|
|
test_delete_confirmation_with_habit_name,
|
|
test_delete_sends_delete_request,
|
|
test_delete_toast_message,
|
|
test_skip_delete_no_console_errors,
|
|
test_typecheck_us011,
|
|
# US-012 tests
|
|
test_filter_bar_exists,
|
|
test_category_filter_options,
|
|
test_status_filter_options,
|
|
test_sort_dropdown_options,
|
|
test_filter_functions_exist,
|
|
test_filter_calls_on_change,
|
|
test_localstorage_persistence,
|
|
test_localstorage_restore,
|
|
test_filter_logic_implementation,
|
|
test_sort_logic_implementation,
|
|
test_backend_provides_should_check_today,
|
|
test_typecheck_us012,
|
|
# US-013 tests
|
|
test_stats_section_exists,
|
|
test_stats_labels_correct,
|
|
test_weekly_summary_exists,
|
|
test_weekly_chart_structure,
|
|
test_weekly_stats_text,
|
|
test_stats_functions_exist,
|
|
test_stats_calculations,
|
|
test_weekly_chart_bars_proportional,
|
|
test_stats_called_from_render,
|
|
test_stats_css_styling,
|
|
test_stats_no_console_errors,
|
|
test_typecheck_us013,
|
|
# US-014 tests
|
|
test_mobile_grid_responsive,
|
|
test_tablet_grid_responsive,
|
|
test_touch_targets_44px,
|
|
test_modals_scrollable_mobile,
|
|
test_pickers_wrap_mobile,
|
|
test_filter_bar_stacks_mobile,
|
|
test_stats_2x2_mobile,
|
|
test_swipe_nav_integration,
|
|
test_mobile_form_inputs,
|
|
test_mobile_no_console_errors,
|
|
test_typecheck_us014,
|
|
test_mobile_day_checkboxes_wrap,
|
|
# US-002 tests (Compact cards)
|
|
test_compact_card_structure,
|
|
test_compact_card_visible_elements,
|
|
test_compact_card_hidden_elements,
|
|
test_progress_percentage_rounded,
|
|
test_compact_card_height_css,
|
|
test_compact_button_circle_style,
|
|
test_next_check_date_function,
|
|
test_compact_card_mobile_friendly,
|
|
test_progress_bar_styling,
|
|
test_compact_card_color_accent,
|
|
test_compact_viewport_320px,
|
|
test_typecheck_us002,
|
|
# US-003 tests (Check/uncheck toggle)
|
|
test_check_button_not_disabled_when_checked,
|
|
test_check_button_clickable_title,
|
|
test_check_button_checked_css,
|
|
test_checkin_habit_toggle_logic,
|
|
test_checkin_optimistic_ui_update,
|
|
test_checkin_error_reversion,
|
|
test_streak_updates_after_toggle,
|
|
test_event_listeners_on_all_buttons,
|
|
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,
|
|
# 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-005, US-006 through US-014...\n")
|
|
|
|
failed = []
|
|
for test in tests:
|
|
try:
|
|
test()
|
|
except AssertionError as e:
|
|
print(f"✗ {test.__name__}: {e}")
|
|
failed.append((test.__name__, str(e)))
|
|
except Exception as e:
|
|
print(f"✗ {test.__name__}: Unexpected error: {e}")
|
|
failed.append((test.__name__, str(e)))
|
|
|
|
print(f"\n{'='*60}")
|
|
if failed:
|
|
print(f"FAILED: {len(failed)} test(s) failed:")
|
|
for name, error in failed:
|
|
print(f" - {name}: {error}")
|
|
sys.exit(1)
|
|
else:
|
|
print(f"SUCCESS: All {len(tests)} tests passed!")
|
|
sys.exit(0)
|
|
|
|
# ==================== US-002 Tests ====================
|
|
# Frontend: Compact habit cards (~100px height)
|
|
|
|
def test_compact_card_structure():
|
|
"""Test 102: Compact card has correct HTML structure"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
html = habits_path.read_text()
|
|
# Check for compact card structure with single row
|
|
assert 'class="habit-card-row"' in html, "Card should have habit-card-row for compact layout"
|
|
assert 'class="habit-card-progress-row"' in html, "Card should have progress-row"
|
|
assert 'class="habit-card-next-date"' in html, "Card should have next-date row"
|
|
print("✓ Test 102: Compact card structure exists")
|
|
|
|
def test_compact_card_visible_elements():
|
|
"""Test 103: Card displays only essential visible elements"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
html = habits_path.read_text()
|
|
# Visible elements
|
|
assert 'habit-card-icon' in html, "Icon should be visible"
|
|
assert 'habit-card-name' in html, "Name should be visible"
|
|
assert 'habit-card-streak' in html, "Streak should be visible"
|
|
assert 'habit-card-check-btn-compact' in html, "Check button should be visible"
|
|
assert 'habit-card-progress' in html, "Progress bar should be visible"
|
|
assert 'habit-card-next-date' in html, "Next date should be visible"
|
|
print("✓ Test 103: Card displays essential visible elements")
|
|
|
|
def test_compact_card_hidden_elements():
|
|
"""Test 104: Card hides non-essential elements"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
html = habits_path.read_text()
|
|
# These should NOT be present in compact design
|
|
assert 'habit-card-lives' not in html, "Lives should be hidden in compact view"
|
|
assert 'habit-card-skip-btn' not in html, "Skip button should be hidden in compact view"
|
|
assert 'habit-card-footer' not in html, "Footer should be hidden in compact view"
|
|
assert 'habit-card-category' not in html, "Category should be hidden in compact view"
|
|
assert 'habit-card-priority' not in html, "Priority should be hidden in compact view"
|
|
assert 'habit-card-last-check' not in html, "Last check info should be hidden in compact view"
|
|
print("✓ Test 104: Card hides non-essential elements")
|
|
|
|
def test_progress_percentage_rounded():
|
|
"""Test 105: Progress percentage is rounded to integer"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
html = habits_path.read_text()
|
|
# Check for Math.round in renderHabitCard
|
|
assert 'Math.round(habit.completion_rate_30d' in html, "Progress should use Math.round()"
|
|
# Check there's no decimal formatting like .toFixed()
|
|
assert 'completion_rate_30d || 0).toFixed' not in html, "Should not use toFixed for progress"
|
|
print("✓ Test 105: Progress percentage uses Math.round()")
|
|
|
|
def test_compact_card_height_css():
|
|
"""Test 106: Card has compact height constraints in CSS"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
html = habits_path.read_text()
|
|
# Check for height constraints
|
|
assert 'min-height: 90px' in html or 'min-height:90px' in html, "Card should have min-height ~90-100px"
|
|
assert 'max-height: 110px' in html or 'max-height: 120px' in html, "Card should have max-height constraint"
|
|
print("✓ Test 106: Card has compact height CSS constraints")
|
|
|
|
def test_compact_button_circle_style():
|
|
"""Test 107: Check button is compact circular style"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
html = habits_path.read_text()
|
|
assert 'habit-card-check-btn-compact' in html, "Should have compact check button class"
|
|
# Check for circular styling
|
|
assert 'border-radius: 50%' in html or 'border-radius:50%' in html, "Compact button should be circular"
|
|
# Check for compact dimensions
|
|
assert any('32px' in html for _ in range(2)), "Compact button should be ~32px"
|
|
print("✓ Test 107: Check button is compact circular style")
|
|
|
|
def test_next_check_date_function():
|
|
"""Test 108: getNextCheckDate function exists and is used"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
html = habits_path.read_text()
|
|
assert 'function getNextCheckDate' in html, "getNextCheckDate function should exist"
|
|
assert 'getNextCheckDate(habit)' in html, "getNextCheckDate should be called in renderHabitCard"
|
|
# Check for expected output strings
|
|
assert "'Next: Tomorrow'" in html or '"Next: Tomorrow"' in html, "Should show 'Next: Tomorrow'"
|
|
assert "'Due: Today'" in html or '"Due: Today"' in html, "Should show 'Due: Today'"
|
|
assert "'Next: Upcoming'" in html or '"Next: Upcoming"' in html, "Should show 'Next: Upcoming'"
|
|
print("✓ Test 108: getNextCheckDate function exists and is used")
|
|
|
|
def test_compact_card_mobile_friendly():
|
|
"""Test 109: Compact cards remain compact on mobile"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
html = habits_path.read_text()
|
|
# Check mobile media query maintains compact size
|
|
mobile_section = html[html.find('@media (max-width: 768px)'):html.find('@media (max-width: 768px)') + 2000] if '@media (max-width: 768px)' in html else ''
|
|
assert 'habit-card-check-btn-compact' in mobile_section, "Mobile styles should include compact button"
|
|
assert 'min-height: 44px' in mobile_section, "Mobile touch targets should be 44px"
|
|
print("✓ Test 109: Compact cards are mobile-friendly")
|
|
|
|
def test_progress_bar_styling():
|
|
"""Test 110: Progress bar has proper styling"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
html = habits_path.read_text()
|
|
assert 'habit-card-progress-bar' in html, "Progress bar container should exist"
|
|
assert 'habit-card-progress-fill' in html, "Progress fill element should exist"
|
|
assert 'habit-card-progress-text' in html, "Progress text should exist"
|
|
# Check for dynamic width styling
|
|
assert 'width: ${completionRate}%' in html, "Progress fill should have dynamic width"
|
|
print("✓ Test 110: Progress bar styling exists")
|
|
|
|
def test_compact_card_color_accent():
|
|
"""Test 111: Card uses habit color for border and progress"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
html = habits_path.read_text()
|
|
# Check border-left-color uses habit.color
|
|
assert 'border-left-color: ${habit.color}' in html, "Card should use habit color for left border"
|
|
# Check progress bar uses habit.color
|
|
assert 'background-color: ${habit.color}' in html, "Progress bar should use habit color"
|
|
print("✓ Test 111: Card uses habit color for accents")
|
|
|
|
def test_compact_viewport_320px():
|
|
"""Test 112: Cards render correctly at 320px viewport"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
html = habits_path.read_text()
|
|
# Check for text-overflow ellipsis for long names
|
|
assert 'text-overflow: ellipsis' in html, "Long names should use ellipsis"
|
|
assert 'white-space: nowrap' in html, "Card name should not wrap"
|
|
# Check for flex-shrink on action elements
|
|
assert 'flex-shrink: 0' in html, "Action buttons should not shrink"
|
|
print("✓ Test 112: Cards handle 320px viewport width")
|
|
|
|
def test_typecheck_us002():
|
|
"""Test 113: Typecheck passes for US-002 changes"""
|
|
repo_root = Path(__file__).parent.parent.parent
|
|
result = subprocess.run(
|
|
['python3', '-m', 'py_compile', 'dashboard/api.py'],
|
|
cwd=repo_root,
|
|
capture_output=True,
|
|
text=True
|
|
)
|
|
assert result.returncode == 0, f"Typecheck failed: {result.stderr}"
|
|
print("✓ Test 113: Typecheck passes")
|
|
|
|
# ===== US-003: Frontend check/uncheck toggle behavior =====
|
|
|
|
def test_check_button_not_disabled_when_checked():
|
|
"""Test 114: Check button is not disabled when habit is checked today"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
html = habits_path.read_text()
|
|
# Check that button does not use disabled attribute anymore
|
|
assert 'isDoneToday ? \'disabled\' : \'\'' not in html, "Button should not use disabled attribute"
|
|
# Check that button uses 'checked' class instead
|
|
assert 'isDoneToday ? \'checked\' : \'\'' in html, "Button should use 'checked' class"
|
|
print("✓ Test 114: Check button is not disabled when checked")
|
|
|
|
def test_check_button_clickable_title():
|
|
"""Test 115: Check button has clickable title for both states"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
html = habits_path.read_text()
|
|
# Check titles indicate toggle behavior
|
|
assert 'Click to uncheck' in html, "Checked button should say 'Click to uncheck'"
|
|
assert 'Check in' in html, "Unchecked button should say 'Check in'"
|
|
print("✓ Test 115: Button titles indicate toggle behavior")
|
|
|
|
def test_check_button_checked_css():
|
|
"""Test 116: Check button has CSS for checked state (not disabled)"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
html = habits_path.read_text()
|
|
# Check for .checked class styling (not :disabled)
|
|
assert '.habit-card-check-btn-compact.checked' in html, "Should have .checked class styling"
|
|
assert 'checked:hover' in html.lower(), "Checked button should have hover state"
|
|
# Check that old :disabled styling is replaced
|
|
disabled_count = html.count('.habit-card-check-btn-compact:disabled')
|
|
assert disabled_count == 0, "Should not have :disabled styling for check button"
|
|
print("✓ Test 116: Check button uses .checked class, not :disabled")
|
|
|
|
def test_checkin_habit_toggle_logic():
|
|
"""Test 117: checkInHabit function implements toggle logic"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
js = habits_path.read_text()
|
|
# Check that function finds habit and checks current state
|
|
assert 'const isChecked = isCheckedToday(habit)' in js, "Should check if habit is already checked"
|
|
# Check that it sends DELETE for unchecking
|
|
assert "method: 'DELETE'" in js, "Should send DELETE request for uncheck"
|
|
# Check conditional URL for DELETE
|
|
assert '/check?date=' in js, "DELETE should include date parameter"
|
|
print("✓ Test 117: checkInHabit implements toggle logic")
|
|
|
|
def test_checkin_optimistic_ui_update():
|
|
"""Test 118: Toggle updates UI optimistically before API call"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
js = habits_path.read_text()
|
|
# Check for optimistic UI update
|
|
assert 'btn.innerHTML = \'○\'' in js, "Should update button to unchecked optimistically"
|
|
assert 'btn.innerHTML = \'✓\'' in js, "Should update button to checked optimistically"
|
|
# Check for state storage for rollback
|
|
assert 'originalButtonText' in js, "Should store original button text"
|
|
assert 'originalButtonDisabled' in js, "Should store original button state"
|
|
print("✓ Test 118: Toggle uses optimistic UI updates")
|
|
|
|
def test_checkin_error_reversion():
|
|
"""Test 119: Toggle reverts UI on error"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
js = habits_path.read_text()
|
|
# Check for error handling and reversion
|
|
assert 'btn.innerHTML = originalButtonText' in js, "Should revert button text on error"
|
|
assert 'btn.disabled = originalButtonDisabled' in js, "Should revert button state on error"
|
|
assert 'originalStreakText' in js, "Should store original streak for reversion"
|
|
print("✓ Test 119: Toggle reverts UI on error")
|
|
|
|
def test_streak_updates_after_toggle():
|
|
"""Test 120: Streak display updates after check/uncheck"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
js = habits_path.read_text()
|
|
# Check for streak update
|
|
assert 'streakElement.textContent' in js, "Should update streak element"
|
|
assert 'updatedHabit.streak?.current' in js, "Should use updated streak from response"
|
|
print("✓ Test 120: Streak updates after toggle")
|
|
|
|
def test_event_listeners_on_all_buttons():
|
|
"""Test 121: Event listeners attached to all check buttons (checked and unchecked)"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
js = habits_path.read_text()
|
|
# Check that forEach no longer filters by isCheckedToday
|
|
assert 'habits.forEach(habit =>' in js, "Should iterate all habits"
|
|
# Check that checked buttons get simple click listener
|
|
assert "btn.addEventListener('click'," in js, "Checked buttons should have click listener"
|
|
print("✓ Test 121: Event listeners on all check buttons")
|
|
|
|
def test_toast_messages_for_toggle():
|
|
"""Test 122: Different toast messages for check vs uncheck"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
js = habits_path.read_text()
|
|
# Check for conditional toast messages
|
|
assert 'Check-in removed' in js or 'removed' in js.lower(), "Should have uncheck message"
|
|
assert 'Habit checked!' in js, "Should have check message"
|
|
print("✓ Test 122: Toast messages distinguish check from uncheck")
|
|
|
|
def test_delete_request_format():
|
|
"""Test 123: DELETE request includes date parameter in query string"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
js = habits_path.read_text()
|
|
# Check that DELETE URL includes date query param
|
|
assert '/check?date=${today}' in js, "DELETE should include ?date=YYYY-MM-DD parameter"
|
|
# Check today is formatted as ISO date
|
|
assert "toISOString().split('T')[0]" in js, "Should format today as YYYY-MM-DD"
|
|
print("✓ Test 123: DELETE request has proper date parameter")
|
|
|
|
def test_typecheck_us003():
|
|
"""Test 124: Typecheck passes for US-003 changes"""
|
|
repo_root = Path(__file__).parent.parent.parent
|
|
result = subprocess.run(
|
|
['python3', '-m', 'py_compile', 'dashboard/api.py'],
|
|
cwd=repo_root,
|
|
capture_output=True,
|
|
text=True
|
|
)
|
|
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")
|
|
|
|
# ========== 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()
|