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 @@
Obișnuință nouă
-
+
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)