feat: 14.0 - Frontend - Responsive mobile design
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -472,7 +500,7 @@
|
||||
<h2 class="modal-title">Obișnuință nouă</h2>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Nume *</label>
|
||||
<input type="text" class="input" id="habitName" placeholder="ex: Bazin, Sală, Meditație...">
|
||||
<input type="text" class="input" id="habitName" placeholder="ex: Bazin, Sală, Meditație..." autocapitalize="words" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Frecvență</label>
|
||||
|
||||
256
dashboard/test_habits_mobile.py
Normal file
256
dashboard/test_habits_mobile.py
Normal file
@@ -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'<input[^>]+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 '<script src="/echo/swipe-nav.js"></script>' in content, \
|
||||
"Should have proper script tag for swipe-nav.js"
|
||||
|
||||
# Check for viewport meta tag (required for proper mobile rendering)
|
||||
assert '<meta name="viewport"' in content, "Should have viewport meta tag"
|
||||
assert 'width=device-width' in content, "Viewport should include width=device-width"
|
||||
assert 'initial-scale=1.0' in content, "Viewport should include initial-scale=1.0"
|
||||
|
||||
print("✓ Swipe navigation is enabled")
|
||||
|
||||
def test_mobile_button_sizing():
|
||||
"""Additional test: Verify all interactive elements have proper mobile sizing"""
|
||||
path = Path(__file__).parent / 'habits.html'
|
||||
content = path.read_text()
|
||||
|
||||
# Find the mobile section
|
||||
media_start = content.find('@media (max-width: 768px)')
|
||||
mobile_chunk = content[media_start:media_start + 3000]
|
||||
|
||||
# Check for add-habit-btn sizing
|
||||
assert '.add-habit-btn {' in mobile_chunk or '.add-habit-btn{' in mobile_chunk, "Should have .add-habit-btn mobile styles"
|
||||
assert 'min-height: 44px' in mobile_chunk, "Add habit button should have min-height 44px"
|
||||
|
||||
# Check for generic .btn sizing
|
||||
assert '.btn {' in mobile_chunk or '.btn{' in mobile_chunk, "Should have .btn mobile styles"
|
||||
|
||||
# Check for radio labels sizing
|
||||
assert '.radio-label {' in mobile_chunk or '.radio-label{' in mobile_chunk, "Should have .radio-label mobile styles"
|
||||
|
||||
print("✓ All buttons and interactive elements have proper mobile sizing")
|
||||
|
||||
def test_responsive_layout_structure():
|
||||
"""Additional test: Verify responsive layout structure"""
|
||||
path = Path(__file__).parent / 'habits.html'
|
||||
content = path.read_text()
|
||||
|
||||
# Find the mobile section
|
||||
media_start = content.find('@media (max-width: 768px)')
|
||||
mobile_chunk = content[media_start:media_start + 3000]
|
||||
|
||||
# Verify main padding is adjusted for mobile
|
||||
assert '.main {' in mobile_chunk or '.main{' in mobile_chunk, "Should have .main mobile styles"
|
||||
main_section_start = mobile_chunk.find('.main')
|
||||
main_section = mobile_chunk[main_section_start:main_section_start + 200]
|
||||
assert 'padding' in main_section, "Main should have adjusted padding on mobile"
|
||||
|
||||
print("✓ Responsive layout structure is correct")
|
||||
|
||||
def test_all_acceptance_criteria():
|
||||
"""Summary test: Verify all 6 acceptance criteria are met"""
|
||||
path = Path(__file__).parent / 'habits.html'
|
||||
content = path.read_text()
|
||||
|
||||
# Find mobile section
|
||||
media_start = content.find('@media (max-width: 768px)')
|
||||
mobile_chunk = content[media_start:media_start + 3000]
|
||||
|
||||
# AC1: Modal is full-screen on mobile
|
||||
assert '.modal {' in mobile_chunk or '.modal{' in mobile_chunk, "AC1: Modal styles in mobile section"
|
||||
assert 'width: 100%' in mobile_chunk, "AC1: Modal full-screen width"
|
||||
assert 'height: 100vh' in mobile_chunk, "AC1: Modal full-screen height"
|
||||
|
||||
# AC2: Habit cards stack vertically
|
||||
habits_list_match = re.search(r'\.habits-list\s*\{([^}]+)\}', content)
|
||||
assert habits_list_match and 'flex-direction: column' in habits_list_match.group(1), "AC2: Cards stack vertically"
|
||||
|
||||
# AC3: Touch targets >= 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'<input[^>]+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)
|
||||
Reference in New Issue
Block a user