diff --git a/dashboard/habits.html b/dashboard/habits.html index 92e736d..59b3455 100644 --- a/dashboard/habits.html +++ b/dashboard/habits.html @@ -383,14 +383,42 @@ height: 18px; } + /* Touch targets >= 44x44px for accessibility */ .habit-checkbox { - width: 28px; - height: 28px; + width: 44px; + height: 44px; } .habit-checkbox svg { - width: 16px; - height: 16px; + width: 20px; + height: 20px; + } + + /* Full-screen modal on mobile */ + .modal { + width: 100%; + max-width: 100%; + height: 100vh; + max-height: 100vh; + border-radius: 0; + padding: var(--space-5); + } + + /* Larger touch targets for buttons */ + .add-habit-btn { + padding: var(--space-4) var(--space-5); + min-height: 44px; + } + + .btn { + min-height: 44px; + padding: var(--space-3) var(--space-5); + } + + /* Larger radio buttons for touch */ + .radio-label { + padding: var(--space-4); + min-height: 44px; } } @@ -472,7 +500,7 @@
- +
diff --git a/dashboard/test_habits_mobile.py b/dashboard/test_habits_mobile.py new file mode 100644 index 0000000..ab78333 --- /dev/null +++ b/dashboard/test_habits_mobile.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +""" +Test suite for Story 14.0: Frontend - Responsive mobile design +Tests mobile responsiveness for habit tracker +""" + +import re +from pathlib import Path + +def test_file_exists(): + """AC: Test file exists""" + path = Path(__file__).parent / 'habits.html' + assert path.exists(), "habits.html should exist" + print("✓ File exists") + +def test_modal_fullscreen_mobile(): + """AC1: Modal is full-screen on mobile (< 768px)""" + path = Path(__file__).parent / 'habits.html' + content = path.read_text() + + # Check for mobile media query + assert '@media (max-width: 768px)' in content, "Should have mobile media query" + + # Find the mobile section by locating the media query and extracting content until the closing brace + media_start = content.find('@media (max-width: 768px)') + assert media_start != -1, "Should have mobile media query" + + # Extract a reasonable chunk after the media query (enough to include all mobile styles) + mobile_chunk = content[media_start:media_start + 3000] + + # Check for modal full-screen styles within mobile section + assert '.modal {' in mobile_chunk or '.modal{' in mobile_chunk, "Mobile section should include .modal styles" + assert 'width: 100%' in mobile_chunk, "Modal should have 100% width on mobile" + assert 'height: 100vh' in mobile_chunk, "Modal should have 100vh height on mobile" + assert 'max-height: 100vh' in mobile_chunk, "Modal should have 100vh max-height on mobile" + assert 'border-radius: 0' in mobile_chunk, "Modal should have no border-radius on mobile" + + print("✓ Modal is full-screen on mobile") + +def test_habit_cards_stack_vertically(): + """AC2: Habit cards stack vertically on mobile""" + path = Path(__file__).parent / 'habits.html' + content = path.read_text() + + # Check for habits-list with flex-direction: column + assert '.habits-list' in content, "Should have .habits-list class" + + # Extract habits-list styles + habits_list_match = re.search(r'\.habits-list\s*\{([^}]+)\}', content) + assert habits_list_match, "Should have .habits-list styles" + + habits_list_styles = habits_list_match.group(1) + assert 'display: flex' in habits_list_styles or 'display:flex' in habits_list_styles, "habits-list should use flexbox" + assert 'flex-direction: column' in habits_list_styles or 'flex-direction:column' in habits_list_styles, "habits-list should stack vertically" + + # Find the mobile section + media_start = content.find('@media (max-width: 768px)') + mobile_chunk = content[media_start:media_start + 3000] + + # Verify cards are full width on mobile + assert '.habit-card {' in mobile_chunk or '.habit-card{' in mobile_chunk, "Should have .habit-card mobile styles" + assert 'width: 100%' in mobile_chunk, "Should have 100% width on mobile" + + print("✓ Habit cards stack vertically on mobile") + +def test_touch_targets_44px(): + """AC3: Touch targets >= 44x44px for checkbox""" + path = Path(__file__).parent / 'habits.html' + content = path.read_text() + + # Find the mobile section + media_start = content.find('@media (max-width: 768px)') + assert media_start != -1, "Should have mobile media query" + + mobile_chunk = content[media_start:media_start + 3000] + + # Check for checkbox sizing in mobile section + assert '.habit-checkbox {' in mobile_chunk or '.habit-checkbox{' in mobile_chunk, "Should have .habit-checkbox styles in mobile section" + + # Extract width and height values from the mobile checkbox section + checkbox_section_start = mobile_chunk.find('.habit-checkbox') + checkbox_section = mobile_chunk[checkbox_section_start:checkbox_section_start + 300] + + width_match = re.search(r'width:\s*(\d+)px', checkbox_section) + height_match = re.search(r'height:\s*(\d+)px', checkbox_section) + + assert width_match, "Checkbox should have width specified" + assert height_match, "Checkbox should have height specified" + + width = int(width_match.group(1)) + height = int(height_match.group(1)) + + # Verify touch target size (44x44px minimum for accessibility) + assert width >= 44, f"Checkbox width should be >= 44px (got {width}px)" + assert height >= 44, f"Checkbox height should be >= 44px (got {height}px)" + + # Check for other touch targets (buttons) + assert 'min-height: 44px' in mobile_chunk, "Buttons should have min-height of 44px" + + print("✓ Touch targets are >= 44x44px") + +def test_mobile_optimized_keyboards(): + """AC4: Form inputs use mobile-optimized keyboards""" + path = Path(__file__).parent / 'habits.html' + content = path.read_text() + + # Check for input field + assert 'id="habitName"' in content, "Should have habitName input field" + + # Extract input element + input_match = re.search(r']+id="habitName"[^>]*>', content) + assert input_match, "Should have habitName input element" + + input_element = input_match.group(0) + + # Check for mobile-optimized attributes + # autocapitalize="words" for proper names + # autocomplete="off" to prevent autofill issues + assert 'autocapitalize="words"' in input_element or 'autocapitalize=\'words\'' in input_element, \ + "Input should have autocapitalize='words' for mobile optimization" + assert 'autocomplete="off"' in input_element or 'autocomplete=\'off\'' in input_element, \ + "Input should have autocomplete='off' to prevent autofill" + + # Verify type="text" is present (appropriate for habit names) + assert 'type="text"' in input_element, "Input should have type='text'" + + print("✓ Form inputs use mobile-optimized keyboards") + +def test_swipe_navigation(): + """AC5: Swipe navigation works (via swipe-nav.js)""" + path = Path(__file__).parent / 'habits.html' + content = path.read_text() + + # Check for swipe-nav.js inclusion + assert 'swipe-nav.js' in content, "Should include swipe-nav.js for mobile swipe navigation" + + # Verify script tag + assert '' in content, \ + "Should have proper script tag for swipe-nav.js" + + # Check for viewport meta tag (required for proper mobile rendering) + assert '= 44x44px + checkbox_section_start = mobile_chunk.find('.habit-checkbox') + checkbox_section = mobile_chunk[checkbox_section_start:checkbox_section_start + 300] + assert 'width: 44px' in checkbox_section, "AC3: Touch targets 44px width" + assert 'height: 44px' in checkbox_section, "AC3: Touch targets 44px height" + + # AC4: Mobile-optimized keyboards + input_match = re.search(r']+id="habitName"[^>]*>', content) + assert input_match and 'autocapitalize="words"' in input_match.group(0), "AC4: Mobile keyboards" + + # AC5: Swipe navigation + assert 'swipe-nav.js' in content, "AC5: Swipe navigation" + + # AC6: Tests pass (this test itself) + print("✓ All 6 acceptance criteria verified") + +def main(): + """Run all tests""" + tests = [ + test_file_exists, + test_modal_fullscreen_mobile, + test_habit_cards_stack_vertically, + test_touch_targets_44px, + test_mobile_optimized_keyboards, + test_swipe_navigation, + test_mobile_button_sizing, + test_responsive_layout_structure, + test_all_acceptance_criteria + ] + + print("Running Story 14.0 mobile responsiveness tests...\n") + + failed = [] + for test in tests: + try: + test() + except AssertionError as e: + failed.append((test.__name__, str(e))) + print(f"✗ {test.__name__}: {e}") + + print(f"\n{'='*60}") + if failed: + print(f"FAILED: {len(failed)} test(s) failed") + for name, error in failed: + print(f" - {name}: {error}") + return False + else: + print(f"SUCCESS: All {len(tests)} tests passed! ✓") + return True + +if __name__ == '__main__': + import sys + sys.exit(0 if main() else 1)