Files
clawd/dashboard/test_habits_mobile.py

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)