feat: US-008 - Frontend - Create habit modal with all options
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
Test suite for Habits frontend page structure and navigation
|
||||
Story US-006: Frontend - Page structure, layout, and navigation link
|
||||
Story US-007: Frontend - Habit card component
|
||||
Story US-008: Frontend - Create habit modal with all options
|
||||
"""
|
||||
|
||||
import sys
|
||||
@@ -308,6 +309,205 @@ def test_typecheck_us007():
|
||||
|
||||
print("✓ Test 21: Typecheck passes (all functions defined)")
|
||||
|
||||
def test_modal_opens_on_add_habit_click():
|
||||
"""Test 22: Modal opens when clicking 'Add Habit' button"""
|
||||
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||
content = habits_path.read_text()
|
||||
|
||||
assert 'showAddHabitModal()' in content, "Add Habit button should call showAddHabitModal()"
|
||||
assert 'function showAddHabitModal(' in content, "showAddHabitModal function should be defined"
|
||||
assert 'modal-overlay' in content or 'habitModal' in content, "Should have modal overlay element"
|
||||
print("✓ Test 22: Modal opens on Add Habit button click")
|
||||
|
||||
def test_modal_closes_on_x_and_outside_click():
|
||||
"""Test 23: Modal closes on X button or clicking outside"""
|
||||
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||
content = habits_path.read_text()
|
||||
|
||||
assert 'closeHabitModal()' in content, "Should have closeHabitModal function"
|
||||
assert 'modal-close' in content or 'onclick="closeHabitModal()"' in content, \
|
||||
"X button should call closeHabitModal()"
|
||||
|
||||
# Check for click outside handler
|
||||
assert 'e.target === modal' in content or 'event.target' in content, \
|
||||
"Should handle clicking outside modal"
|
||||
print("✓ Test 23: Modal closes on X button and clicking outside")
|
||||
|
||||
def test_modal_has_all_form_fields():
|
||||
"""Test 24: Form has all required fields"""
|
||||
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||
content = habits_path.read_text()
|
||||
|
||||
# Required fields
|
||||
assert 'habitName' in content or 'name' in content.lower(), "Form should have name field"
|
||||
assert 'habitCategory' in content or 'category' in content.lower(), "Form should have category field"
|
||||
assert 'habitPriority' in content or 'priority' in content.lower(), "Form should have priority field"
|
||||
assert 'habitNotes' in content or 'notes' in content.lower(), "Form should have notes field"
|
||||
assert 'frequencyType' in content or 'frequency' in content.lower(), "Form should have frequency field"
|
||||
assert 'reminderTime' in content or 'reminder' in content.lower(), "Form should have reminder time field"
|
||||
|
||||
print("✓ Test 24: Form has all required fields")
|
||||
|
||||
def test_color_picker_presets_and_custom():
|
||||
"""Test 25: Color picker shows preset swatches and custom hex input"""
|
||||
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||
content = habits_path.read_text()
|
||||
|
||||
assert 'color-picker' in content or 'colorSwatches' in content or 'color-swatch' in content, \
|
||||
"Should have color picker"
|
||||
assert 'customColor' in content or 'custom' in content.lower(), \
|
||||
"Should have custom color input"
|
||||
assert '#RRGGBB' in content or 'pattern=' in content, \
|
||||
"Custom color should have hex pattern"
|
||||
assert 'presetColors' in content or '#3B82F6' in content or '#EF4444' in content, \
|
||||
"Should have preset colors"
|
||||
|
||||
print("✓ Test 25: Color picker with presets and custom hex")
|
||||
|
||||
def test_icon_picker_grid():
|
||||
"""Test 26: Icon picker shows grid of common Lucide icons"""
|
||||
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||
content = habits_path.read_text()
|
||||
|
||||
assert 'icon-picker' in content or 'iconPicker' in content, \
|
||||
"Should have icon picker"
|
||||
assert 'icon-option' in content or 'commonIcons' in content, \
|
||||
"Should have icon options"
|
||||
assert 'selectIcon' in content, "Should have selectIcon function"
|
||||
|
||||
# Check for common icons
|
||||
icon_count = sum([1 for icon in ['dumbbell', 'moon', 'book', 'brain', 'heart']
|
||||
if icon in content])
|
||||
assert icon_count >= 3, "Should have at least 3 common icons"
|
||||
|
||||
print("✓ Test 26: Icon picker with grid of Lucide icons")
|
||||
|
||||
def test_frequency_params_conditional():
|
||||
"""Test 27: Frequency params display conditionally based on type"""
|
||||
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||
content = habits_path.read_text()
|
||||
|
||||
assert 'updateFrequencyParams' in content, "Should have updateFrequencyParams function"
|
||||
assert 'frequencyParams' in content, "Should have frequency params container"
|
||||
assert 'specific_days' in content, "Should handle specific_days frequency"
|
||||
assert 'x_per_week' in content, "Should handle x_per_week frequency"
|
||||
assert 'custom' in content.lower(), "Should handle custom frequency"
|
||||
|
||||
# Check for conditional rendering (day checkboxes for specific_days)
|
||||
assert 'day-checkbox' in content or "['Mon', 'Tue'" in content or 'Mon' in content, \
|
||||
"Should have day checkboxes for specific_days"
|
||||
|
||||
print("✓ Test 27: Frequency params display conditionally")
|
||||
|
||||
def test_client_side_validation():
|
||||
"""Test 28: Client-side validation prevents submit without name"""
|
||||
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||
content = habits_path.read_text()
|
||||
|
||||
assert 'required' in content, "Name field should be required"
|
||||
assert 'trim()' in content, "Should trim input values"
|
||||
|
||||
# Check for validation in submit function
|
||||
submit_func = content[content.find('function submitHabitForm'):]
|
||||
assert 'if (!name)' in submit_func or 'name.length' in submit_func, \
|
||||
"Should validate name is not empty"
|
||||
assert 'showToast' in submit_func and 'error' in submit_func, \
|
||||
"Should show error toast for validation failures"
|
||||
|
||||
print("✓ Test 28: Client-side validation checks name required")
|
||||
|
||||
def test_submit_posts_to_api():
|
||||
"""Test 29: Submit sends POST /echo/api/habits and refreshes list"""
|
||||
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||
content = habits_path.read_text()
|
||||
|
||||
assert 'submitHabitForm' in content, "Should have submitHabitForm function"
|
||||
|
||||
submit_func = content[content.find('function submitHabitForm'):]
|
||||
assert "fetch('/echo/api/habits'" in submit_func or 'fetch("/echo/api/habits"' in submit_func, \
|
||||
"Should POST to /echo/api/habits"
|
||||
assert "'POST'" in submit_func or '"POST"' in submit_func, \
|
||||
"Should use POST method"
|
||||
assert 'JSON.stringify' in submit_func, "Should send JSON body"
|
||||
assert 'loadHabits()' in submit_func, "Should refresh habit list on success"
|
||||
|
||||
print("✓ Test 29: Submit POSTs to API and refreshes list")
|
||||
|
||||
def test_loading_state_on_submit():
|
||||
"""Test 30: Loading state shown on submit button during API call"""
|
||||
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||
content = habits_path.read_text()
|
||||
|
||||
submit_func = content[content.find('function submitHabitForm'):]
|
||||
assert 'disabled = true' in submit_func or '.disabled' in submit_func, \
|
||||
"Submit button should be disabled during API call"
|
||||
assert 'Creating' in submit_func or 'loading' in submit_func.lower(), \
|
||||
"Should show loading text"
|
||||
assert 'disabled = false' in submit_func, \
|
||||
"Submit button should be re-enabled after API call"
|
||||
|
||||
print("✓ Test 30: Loading state on submit button")
|
||||
|
||||
def test_toast_notifications():
|
||||
"""Test 31: Toast notification shown for success and error"""
|
||||
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||
content = habits_path.read_text()
|
||||
|
||||
assert 'showToast' in content, "Should have showToast function"
|
||||
assert 'toast' in content, "Should have toast styling"
|
||||
|
||||
toast_func = content[content.find('function showToast'):]
|
||||
assert 'success' in toast_func and 'error' in toast_func, \
|
||||
"Toast should handle both success and error types"
|
||||
assert 'check-circle' in toast_func or 'alert-circle' in toast_func, \
|
||||
"Toast should show appropriate icons"
|
||||
assert 'setTimeout' in toast_func or 'remove()' in toast_func, \
|
||||
"Toast should auto-dismiss"
|
||||
|
||||
print("✓ Test 31: Toast notifications for success and error")
|
||||
|
||||
def test_modal_no_console_errors():
|
||||
"""Test 32: No obvious console error sources in modal code"""
|
||||
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||
content = habits_path.read_text()
|
||||
|
||||
# Check that modal functions exist
|
||||
assert 'function showAddHabitModal(' in content, "showAddHabitModal should be defined"
|
||||
assert 'function closeHabitModal(' in content, "closeHabitModal should be defined"
|
||||
assert 'function submitHabitForm(' in content, "submitHabitForm should be defined"
|
||||
assert 'function updateFrequencyParams(' in content, "updateFrequencyParams should be defined"
|
||||
|
||||
# Check for proper error handling
|
||||
submit_func = content[content.find('function submitHabitForm'):]
|
||||
assert 'try' in submit_func and 'catch' in submit_func, \
|
||||
"Submit function should have try-catch error handling"
|
||||
|
||||
print("✓ Test 32: No obvious console error sources")
|
||||
|
||||
def test_typecheck_us008():
|
||||
"""Test 33: Typecheck passes for US-008 (all modal functions defined)"""
|
||||
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||
content = habits_path.read_text()
|
||||
|
||||
# Check all new functions are defined
|
||||
required_functions = [
|
||||
'showAddHabitModal',
|
||||
'closeHabitModal',
|
||||
'initColorPicker',
|
||||
'selectColor',
|
||||
'initIconPicker',
|
||||
'selectIcon',
|
||||
'updateFrequencyParams',
|
||||
'submitHabitForm',
|
||||
'showToast'
|
||||
]
|
||||
|
||||
for func in required_functions:
|
||||
assert f'function {func}(' in content or f'const {func} =' in content, \
|
||||
f"{func} function should be defined"
|
||||
|
||||
print("✓ Test 33: Typecheck passes (all modal functions defined)")
|
||||
|
||||
def run_all_tests():
|
||||
"""Run all tests in sequence"""
|
||||
tests = [
|
||||
@@ -334,9 +534,22 @@ def run_all_tests():
|
||||
test_card_lucide_createicons,
|
||||
test_card_common_css_variables,
|
||||
test_typecheck_us007,
|
||||
# US-008 tests
|
||||
test_modal_opens_on_add_habit_click,
|
||||
test_modal_closes_on_x_and_outside_click,
|
||||
test_modal_has_all_form_fields,
|
||||
test_color_picker_presets_and_custom,
|
||||
test_icon_picker_grid,
|
||||
test_frequency_params_conditional,
|
||||
test_client_side_validation,
|
||||
test_submit_posts_to_api,
|
||||
test_loading_state_on_submit,
|
||||
test_toast_notifications,
|
||||
test_modal_no_console_errors,
|
||||
test_typecheck_us008,
|
||||
]
|
||||
|
||||
print(f"\nRunning {len(tests)} frontend tests for US-006 and US-007...\n")
|
||||
print(f"\nRunning {len(tests)} frontend tests for US-006, US-007, and US-008...\n")
|
||||
|
||||
failed = []
|
||||
for test in tests:
|
||||
|
||||
Reference in New Issue
Block a user