feat: US-003 - Frontend: Check/uncheck toggle behavior

This commit is contained in:
Echo
2026-02-10 18:50:26 +00:00
parent 081121e48d
commit 9d9f00e069
2 changed files with 236 additions and 44 deletions

View File

@@ -1,6 +1,7 @@
"""
Test suite for Habits frontend page structure and navigation
Story US-002: Frontend - Compact habit cards (~100px height)
Story US-003: Frontend - Check/uncheck toggle behavior
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
@@ -884,7 +885,7 @@ def test_checkin_toast_with_streak():
# Check that streak value comes from API response
checkin_start = content.find('function checkInHabit')
checkin_area = content[checkin_start:checkin_start+2000]
checkin_area = content[checkin_start:checkin_start+5000] # Increased for toggle implementation
assert 'updatedHabit' in checkin_area or 'await response.json()' in checkin_area, \
"Should get updated habit from response"
@@ -893,26 +894,25 @@ def test_checkin_toast_with_streak():
print("✓ Test 50: Toast shows 'Habit checked! 🔥 Streak: X'")
def test_checkin_button_disabled_after():
"""Test 51: Check-in button becomes disabled after check-in (compact design)"""
"""Test 51: Check-in button shows checked state after check-in (toggle design)"""
habits_path = Path(__file__).parent.parent / 'habits.html'
content = habits_path.read_text()
# Check that renderHabitCard uses isCheckedToday to disable button
# Check that renderHabitCard uses isCheckedToday to determine state
assert 'isCheckedToday(habit)' in content, \
"Should check if habit is checked today"
# Check button uses disabled attribute based on condition
assert 'isDoneToday ? \'disabled\' : \'\'' in content or \
'${isDoneToday ? \'disabled\' : \'\'}' in content or \
'disabled' in content, \
"Button should have conditional disabled attribute"
# Check button uses 'checked' class instead of disabled (US-003 toggle behavior)
assert 'isDoneToday ? \'checked\' : \'\'' in content or \
'${isDoneToday ? \'checked\' : \'\'}' in content, \
"Button should use 'checked' class (not disabled)"
# Check button displays checkmark when done (compact design)
assert 'isDoneToday ? \'\' : \'\'' in content or \
'${isDoneToday ? \'\' : \'\'}' in content, \
"Button should show ✓ when disabled, ○ when enabled (compact)"
"Button should show ✓ when checked, ○ when unchecked (compact)"
print("✓ Test 51: Button becomes disabled after check-in (compact design)")
print("✓ Test 51: Button shows checked state after check-in (toggle design)")
def test_checkin_pulse_animation():
"""Test 52: Pulse animation plays on card after successful check-in"""
@@ -926,7 +926,7 @@ def test_checkin_pulse_animation():
# Check that checkInHabit adds pulse class
checkin_start = content.find('function checkInHabit')
checkin_area = content[checkin_start:checkin_start+2000]
checkin_area = content[checkin_start:checkin_start+5000] # Increased for toggle implementation
assert 'pulse' in checkin_area or 'classList.add' in checkin_area, \
"Should add pulse class to card after check-in"
@@ -1725,9 +1725,21 @@ def run_all_tests():
test_compact_card_color_accent,
test_compact_viewport_320px,
test_typecheck_us002,
# US-003 tests (Check/uncheck toggle)
test_check_button_not_disabled_when_checked,
test_check_button_clickable_title,
test_check_button_checked_css,
test_checkin_habit_toggle_logic,
test_checkin_optimistic_ui_update,
test_checkin_error_reversion,
test_streak_updates_after_toggle,
test_event_listeners_on_all_buttons,
test_toast_messages_for_toggle,
test_delete_request_format,
test_typecheck_us003,
]
print(f"\nRunning {len(tests)} frontend tests for US-002, US-006 through US-014...\n")
print(f"\nRunning {len(tests)} frontend tests for US-002, US-003, US-006 through US-014...\n")
failed = []
for test in tests:
@@ -1885,5 +1897,122 @@ def test_typecheck_us002():
assert result.returncode == 0, f"Typecheck failed: {result.stderr}"
print("✓ Test 113: Typecheck passes")
# ===== US-003: Frontend check/uncheck toggle behavior =====
def test_check_button_not_disabled_when_checked():
"""Test 114: Check button is not disabled when habit is checked today"""
habits_path = Path(__file__).parent.parent / 'habits.html'
html = habits_path.read_text()
# Check that button does not use disabled attribute anymore
assert 'isDoneToday ? \'disabled\' : \'\'' not in html, "Button should not use disabled attribute"
# Check that button uses 'checked' class instead
assert 'isDoneToday ? \'checked\' : \'\'' in html, "Button should use 'checked' class"
print("✓ Test 114: Check button is not disabled when checked")
def test_check_button_clickable_title():
"""Test 115: Check button has clickable title for both states"""
habits_path = Path(__file__).parent.parent / 'habits.html'
html = habits_path.read_text()
# Check titles indicate toggle behavior
assert 'Click to uncheck' in html, "Checked button should say 'Click to uncheck'"
assert 'Check in' in html, "Unchecked button should say 'Check in'"
print("✓ Test 115: Button titles indicate toggle behavior")
def test_check_button_checked_css():
"""Test 116: Check button has CSS for checked state (not disabled)"""
habits_path = Path(__file__).parent.parent / 'habits.html'
html = habits_path.read_text()
# Check for .checked class styling (not :disabled)
assert '.habit-card-check-btn-compact.checked' in html, "Should have .checked class styling"
assert 'checked:hover' in html.lower(), "Checked button should have hover state"
# Check that old :disabled styling is replaced
disabled_count = html.count('.habit-card-check-btn-compact:disabled')
assert disabled_count == 0, "Should not have :disabled styling for check button"
print("✓ Test 116: Check button uses .checked class, not :disabled")
def test_checkin_habit_toggle_logic():
"""Test 117: checkInHabit function implements toggle logic"""
habits_path = Path(__file__).parent.parent / 'habits.html'
js = habits_path.read_text()
# Check that function finds habit and checks current state
assert 'const isChecked = isCheckedToday(habit)' in js, "Should check if habit is already checked"
# Check that it sends DELETE for unchecking
assert "method: 'DELETE'" in js, "Should send DELETE request for uncheck"
# Check conditional URL for DELETE
assert '/check?date=' in js, "DELETE should include date parameter"
print("✓ Test 117: checkInHabit implements toggle logic")
def test_checkin_optimistic_ui_update():
"""Test 118: Toggle updates UI optimistically before API call"""
habits_path = Path(__file__).parent.parent / 'habits.html'
js = habits_path.read_text()
# Check for optimistic UI update
assert 'btn.innerHTML = \'\'' in js, "Should update button to unchecked optimistically"
assert 'btn.innerHTML = \'\'' in js, "Should update button to checked optimistically"
# Check for state storage for rollback
assert 'originalButtonText' in js, "Should store original button text"
assert 'originalButtonDisabled' in js, "Should store original button state"
print("✓ Test 118: Toggle uses optimistic UI updates")
def test_checkin_error_reversion():
"""Test 119: Toggle reverts UI on error"""
habits_path = Path(__file__).parent.parent / 'habits.html'
js = habits_path.read_text()
# Check for error handling and reversion
assert 'btn.innerHTML = originalButtonText' in js, "Should revert button text on error"
assert 'btn.disabled = originalButtonDisabled' in js, "Should revert button state on error"
assert 'originalStreakText' in js, "Should store original streak for reversion"
print("✓ Test 119: Toggle reverts UI on error")
def test_streak_updates_after_toggle():
"""Test 120: Streak display updates after check/uncheck"""
habits_path = Path(__file__).parent.parent / 'habits.html'
js = habits_path.read_text()
# Check for streak update
assert 'streakElement.textContent' in js, "Should update streak element"
assert 'updatedHabit.streak?.current' in js, "Should use updated streak from response"
print("✓ Test 120: Streak updates after toggle")
def test_event_listeners_on_all_buttons():
"""Test 121: Event listeners attached to all check buttons (checked and unchecked)"""
habits_path = Path(__file__).parent.parent / 'habits.html'
js = habits_path.read_text()
# Check that forEach no longer filters by isCheckedToday
assert 'habits.forEach(habit =>' in js, "Should iterate all habits"
# Check that checked buttons get simple click listener
assert "btn.addEventListener('click'," in js, "Checked buttons should have click listener"
print("✓ Test 121: Event listeners on all check buttons")
def test_toast_messages_for_toggle():
"""Test 122: Different toast messages for check vs uncheck"""
habits_path = Path(__file__).parent.parent / 'habits.html'
js = habits_path.read_text()
# Check for conditional toast messages
assert 'Check-in removed' in js or 'removed' in js.lower(), "Should have uncheck message"
assert 'Habit checked!' in js, "Should have check message"
print("✓ Test 122: Toast messages distinguish check from uncheck")
def test_delete_request_format():
"""Test 123: DELETE request includes date parameter in query string"""
habits_path = Path(__file__).parent.parent / 'habits.html'
js = habits_path.read_text()
# Check that DELETE URL includes date query param
assert '/check?date=${today}' in js, "DELETE should include ?date=YYYY-MM-DD parameter"
# Check today is formatted as ISO date
assert "toISOString().split('T')[0]" in js, "Should format today as YYYY-MM-DD"
print("✓ Test 123: DELETE request has proper date parameter")
def test_typecheck_us003():
"""Test 124: Typecheck passes for US-003 changes"""
repo_root = Path(__file__).parent.parent.parent
result = subprocess.run(
['python3', '-m', 'py_compile', 'dashboard/api.py'],
cwd=repo_root,
capture_output=True,
text=True
)
assert result.returncode == 0, f"Typecheck failed: {result.stderr}"
print("✓ Test 124: Typecheck passes")
if __name__ == '__main__':
run_all_tests()