257 lines
11 KiB
Python
257 lines
11 KiB
Python
#!/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)
|