Files
clawd/dashboard/tests/test_habits_frontend.py

577 lines
25 KiB
Python

"""
Test suite for Habits frontend page structure and navigation
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
"""
import sys
import os
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 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'):]
assert "fetch('/echo/api/habits'" in submit_func or 'fetch("/echo/api/habits"' in submit_func, \
"Should POST to /echo/api/habits"
assert "'POST'" in submit_func or '"POST"' in submit_func, \
"Should use POST 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 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,
]
print(f"\nRunning {len(tests)} frontend tests for US-006, US-007, and US-008...\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)
if __name__ == '__main__':
run_all_tests()