""" 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) 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 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 '>HabitsHabits') nav_end = content.find('', 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 '') 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 'All' in content, "Should have 'All' option" assert 'value="work">Work' in content, "Should have 'Work' option" assert 'value="health">Health' in content, "Should have 'Health' option" assert 'value="growth">Growth' in content, "Should have 'Growth' option" assert 'value="personal">Personal' 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' in content, "Should have 'All' option" assert 'value="active_today">Active Today' in content, "Should have 'Active Today' option" assert 'value="done_today">Done Today' in content, "Should have 'Done Today' option" assert 'value="overdue">Overdue' 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" assert 'min-height: 48px' in content, "Check-in button should have min-height 48px" 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 bar stacks vertically on mobile""" habits_path = Path(__file__).parent.parent / 'habits.html' content = habits_path.read_text() # Check for filter bar mobile styles mobile_section = content[content.find('@media (max-width: 768px)'):content.find('@media (max-width: 768px)') + 3000] assert '.filter-bar' in mobile_section, "Should have filter-bar mobile styles" assert 'flex-direction: column' in mobile_section, "Filter bar should stack vertically" print("✓ Test 95: Filter bar stacks 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 '' 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, ] print(f"\nRunning {len(tests)} frontend tests for 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) if __name__ == '__main__': run_all_tests()