diff --git a/dashboard/habits.html b/dashboard/habits.html index 8b41432..40db2ef 100644 --- a/dashboard/habits.html +++ b/dashboard/habits.html @@ -198,10 +198,37 @@ .habit-card-lives { display: flex; justify-content: center; - gap: var(--space-1); + align-items: center; + gap: var(--space-2); font-size: var(--text-lg); } + .habit-card-lives-hearts { + display: flex; + gap: var(--space-1); + } + + .habit-card-skip-btn { + background: none; + border: none; + color: var(--text-muted); + font-size: var(--text-xs); + cursor: pointer; + padding: var(--space-1) var(--space-2); + border-radius: var(--radius-sm); + transition: all var(--transition-base); + } + + .habit-card-skip-btn:hover:not(:disabled) { + background: var(--bg-hover); + color: var(--text-primary); + } + + .habit-card-skip-btn:disabled { + cursor: not-allowed; + opacity: 0.3; + } + .habit-card-completion { font-size: var(--text-sm); color: var(--text-muted); @@ -811,6 +838,8 @@ throw new Error(`HTTP ${response.status}`); } habits = await response.json(); + // Cache habits for delete confirmation + localStorage.setItem('habitsCache', JSON.stringify(habits)); renderHabits(); } catch (error) { console.error('Failed to load habits:', error); @@ -900,7 +929,17 @@
${lastCheckInfo}
-
${livesHtml}
+
+
${livesHtml}
+ +
${completionRate}% (30d)
@@ -1335,10 +1374,66 @@ // Delete habit (placeholder) async function deleteHabit(habitId) { - if (!confirm('Are you sure you want to delete this habit?')) { + // Find habit to get its name for confirmation + const habits = JSON.parse(localStorage.getItem('habitsCache') || '[]'); + const habit = habits.find(h => h.id === habitId); + const habitName = habit ? habit.name : 'this habit'; + + if (!confirm(`Delete ${habitName}? This cannot be undone.`)) { return; } - alert('Delete functionality - coming in next story!'); + + try { + const response = await fetch(`/echo/api/habits/${habitId}`, { + method: 'DELETE' + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + // Show success toast + showToast('Habit deleted', 'success'); + + // Refresh habits list (will remove card from DOM) + await loadHabits(); + } catch (error) { + console.error('Failed to delete habit:', error); + showToast(`Failed to delete habit: ${error.message}`, 'error'); + } + } + + // Skip a habit day using a life + async function skipHabitDay(habitId, habitName) { + if (!confirm('Use 1 life to skip today?')) { + return; + } + + try { + const response = await fetch(`/echo/api/habits/${habitId}/skip`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || `HTTP ${response.status}`); + } + + const updatedHabit = await response.json(); + + // Show success toast with remaining lives + const remainingLives = updatedHabit.lives; + showToast(`Day skipped. ${remainingLives} ${remainingLives === 1 ? 'life' : 'lives'} remaining.`, 'success'); + + // Refresh habits list + await loadHabits(); + } catch (error) { + console.error('Failed to skip habit day:', error); + showToast(`Failed to skip: ${error.message}`, 'error'); + } } // Check-in state diff --git a/dashboard/tests/test_habits_frontend.py b/dashboard/tests/test_habits_frontend.py index fb77e78..a67cd19 100644 --- a/dashboard/tests/test_habits_frontend.py +++ b/dashboard/tests/test_habits_frontend.py @@ -5,6 +5,7 @@ Story US-007: Frontend - Habit card component Story US-008: Frontend - Create habit modal with all options Story US-009: Frontend - Edit habit modal Story US-010: Frontend - Check-in interaction (click and long-press) +Story US-011: Frontend - Skip, lives display, and delete confirmation """ import sys @@ -996,6 +997,156 @@ def test_typecheck_us010(): print("✓ Test 55: Typecheck passes (all check-in functions and variables defined)") +# ========== US-011 Tests: Skip, lives display, and delete confirmation ========== + +def test_skip_button_visible(): + """Test 56: Skip button visible next to lives hearts on each card""" + html_path = Path(__file__).parent.parent / 'habits.html' + content = html_path.read_text() + + # Check skip button CSS class exists + assert '.habit-card-skip-btn' in content, "Skip button CSS class should exist" + + # Check skip button is in renderHabitCard + assert 'Skip day' in content, "Skip button text should be 'Skip day'" + assert 'onclick="skipHabitDay' in content, "Skip button should call skipHabitDay function" + + # Check lives display structure has both hearts and button + assert 'habit-card-lives-hearts' in content, "Lives hearts should be wrapped in separate div" + + print("✓ Test 56: Skip button visible next to lives hearts on each card") + +def test_skip_confirmation_dialog(): + """Test 57: Clicking skip shows confirmation dialog""" + html_path = Path(__file__).parent.parent / 'habits.html' + content = html_path.read_text() + + # Check skipHabitDay function exists + assert 'async function skipHabitDay' in content, "skipHabitDay function should exist" + + # Check confirmation dialog + assert 'Use 1 life to skip today?' in content, "Skip confirmation dialog should ask 'Use 1 life to skip today?'" + assert 'confirm(' in content, "Should use confirm dialog" + + print("✓ Test 57: Clicking skip shows confirmation dialog") + +def test_skip_sends_post_and_refreshes(): + """Test 58: Confirming skip sends POST /echo/api/habits/{id}/skip and refreshes card""" + html_path = Path(__file__).parent.parent / 'habits.html' + content = html_path.read_text() + + # Check API endpoint + assert "/echo/api/habits/${habitId}/skip" in content, "Should POST to /echo/api/habits/{id}/skip" + assert "method: 'POST'" in content, "Should use POST method" + + # Check refresh + assert 'await loadHabits()' in content, "Should refresh habits list after skip" + + print("✓ Test 58: Confirming skip sends POST /echo/api/habits/{id}/skip and refreshes card") + +def test_skip_button_disabled_when_no_lives(): + """Test 59: Skip button disabled when lives is 0 with tooltip 'No lives left'""" + html_path = Path(__file__).parent.parent / 'habits.html' + content = html_path.read_text() + + # Check disabled condition + assert "habit.lives === 0 ? 'disabled' : ''" in content, "Skip button should be disabled when lives is 0" + + # Check tooltip + assert 'No lives left' in content, "Tooltip should say 'No lives left' when disabled" + + print("✓ Test 59: Skip button disabled when lives is 0 with tooltip 'No lives left'") + +def test_skip_toast_message(): + """Test 60: Toast shows 'Day skipped. X lives remaining.' after skip""" + html_path = Path(__file__).parent.parent / 'habits.html' + content = html_path.read_text() + + # Check toast message format + assert 'Day skipped' in content, "Toast should include 'Day skipped'" + assert 'remaining' in content, "Toast should show remaining lives" + + # Check life/lives plural handling + assert "remainingLives === 1 ? 'life' : 'lives'" in content, "Should handle singular/plural for lives" + + print("✓ Test 60: Toast shows 'Day skipped. X lives remaining.' after skip") + +def test_delete_confirmation_with_habit_name(): + """Test 61: Clicking trash icon shows delete confirmation dialog with habit name""" + html_path = Path(__file__).parent.parent / 'habits.html' + content = html_path.read_text() + + # Check delete function exists + assert 'async function deleteHabit' in content, "deleteHabit function should exist" + + # Check confirmation uses habit name + assert 'Delete ${habitName}?' in content or 'Delete ' in content, "Delete confirmation should include habit name" + assert 'This cannot be undone' in content, "Delete confirmation should warn 'This cannot be undone'" + + print("✓ Test 61: Clicking trash icon shows delete confirmation dialog with habit name") + +def test_delete_sends_delete_request(): + """Test 62: Confirming delete sends DELETE /echo/api/habits/{id} and removes card""" + html_path = Path(__file__).parent.parent / 'habits.html' + content = html_path.read_text() + + # Check DELETE request + assert "method: 'DELETE'" in content, "Should use DELETE method" + assert "/echo/api/habits/${habitId}" in content or "/echo/api/habits/" in content, "Should DELETE to /echo/api/habits/{id}" + + # Check refresh (removes card from DOM via re-render) + assert 'await loadHabits()' in content, "Should refresh habits list after delete" + + print("✓ Test 62: Confirming delete sends DELETE /echo/api/habits/{id} and removes card") + +def test_delete_toast_message(): + """Test 63: Toast shows 'Habit deleted' after deletion""" + html_path = Path(__file__).parent.parent / 'habits.html' + content = html_path.read_text() + + # Check success toast + assert 'Habit deleted' in content, "Toast should say 'Habit deleted'" + + print("✓ Test 63: Toast shows 'Habit deleted' after deletion") + +def test_skip_delete_no_console_errors(): + """Test 64: No obvious console error sources in skip and delete code""" + html_path = Path(__file__).parent.parent / 'habits.html' + content = html_path.read_text() + + # Check error handling in skipHabitDay + skip_func = content[content.find('async function skipHabitDay'):content.find('async function skipHabitDay') + 1500] + assert 'try' in skip_func and 'catch' in skip_func, "skipHabitDay should have try/catch" + assert 'console.error' in skip_func, "skipHabitDay should log errors" + + # Check error handling in deleteHabit + delete_func = content[content.find('async function deleteHabit'):content.find('async function deleteHabit') + 1500] + assert 'try' in delete_func and 'catch' in delete_func, "deleteHabit should have try/catch" + assert 'console.error' in delete_func, "deleteHabit should log errors" + + print("✓ Test 64: No obvious console error sources in skip and delete code") + +def test_typecheck_us011(): + """Test 65: Typecheck passes (all skip and delete functions defined)""" + html_path = Path(__file__).parent.parent / 'habits.html' + content = html_path.read_text() + + # Check function definitions + functions = [ + 'skipHabitDay', + 'deleteHabit', + ] + + for func in functions: + assert f"function {func}" in content, \ + f"{func} should be defined" + + # Check that habits are cached for delete + assert 'localStorage.setItem' in content, "Should cache habits in localStorage" + assert 'habitsCache' in content, "Should use 'habitsCache' key" + + print("✓ Test 65: Typecheck passes (all skip and delete functions defined)") + def run_all_tests(): """Run all tests in sequence""" tests = [ @@ -1059,9 +1210,20 @@ def run_all_tests(): test_checkin_prevent_context_menu, test_checkin_event_listeners_attached, test_typecheck_us010, + # US-011 tests + test_skip_button_visible, + test_skip_confirmation_dialog, + test_skip_sends_post_and_refreshes, + test_skip_button_disabled_when_no_lives, + test_skip_toast_message, + test_delete_confirmation_with_habit_name, + test_delete_sends_delete_request, + test_delete_toast_message, + test_skip_delete_no_console_errors, + test_typecheck_us011, ] - print(f"\nRunning {len(tests)} frontend tests for US-006, US-007, US-008, US-009, and US-010...\n") + print(f"\nRunning {len(tests)} frontend tests for US-006, US-007, US-008, US-009, US-010, and US-011...\n") failed = [] for test in tests: