1089 lines
49 KiB
Python
1089 lines
49 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
|
|
Story US-009: Frontend - Edit habit modal
|
|
Story US-010: Frontend - Check-in interaction (click and long-press)
|
|
"""
|
|
|
|
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 (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+2000]
|
|
|
|
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 becomes disabled with 'Done today' after check-in"""
|
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
|
content = habits_path.read_text()
|
|
|
|
# Check that renderHabitCard uses isCheckedToday to disable button
|
|
assert 'isCheckedToday(habit)' in content, \
|
|
"Should check if habit is checked today"
|
|
|
|
# Check button uses disabled attribute based on condition
|
|
assert 'isDoneToday ? \'disabled\' : \'\'' in content or \
|
|
'${isDoneToday ? \'disabled\' : \'\'}' in content or \
|
|
'disabled' in content, \
|
|
"Button should have conditional disabled attribute"
|
|
|
|
# Check button text changes
|
|
assert '✓ Done today' in content or 'Done today' in content, \
|
|
"Button should show 'Done today' when disabled"
|
|
assert 'Check In' in content, "Button should show 'Check In' when enabled"
|
|
|
|
print("✓ Test 51: Button becomes disabled with 'Done today' after check-in")
|
|
|
|
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+2000]
|
|
|
|
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)")
|
|
|
|
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,
|
|
]
|
|
|
|
print(f"\nRunning {len(tests)} frontend tests for US-006, US-007, US-008, US-009, and US-010...\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()
|