`;
}
+ // Check if habit was checked today
+ function isCheckedToday(habit) {
+ if (!habit.completions || habit.completions.length === 0) {
+ return false;
+ }
+ const today = new Date().toISOString().split('T')[0];
+ return habit.completions.some(c => c.date === today);
+ }
+
+ // Get last check-in info text
+ function getLastCheckInfo(habit) {
+ if (!habit.completions || habit.completions.length === 0) {
+ return 'Last: Never';
+ }
+
+ const lastCompletion = habit.completions[habit.completions.length - 1];
+ const lastDate = new Date(lastCompletion.date);
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ lastDate.setHours(0, 0, 0, 0);
+
+ const diffDays = Math.floor((today - lastDate) / (1000 * 60 * 60 * 24));
+
+ if (diffDays === 0) {
+ return 'Last: Today';
+ } else if (diffDays === 1) {
+ return 'Last: Yesterday';
+ } else {
+ return `Last: ${diffDays} days ago`;
+ }
+ }
+
+ // Render lives as hearts
+ function renderLives(lives) {
+ const totalLives = 3;
+ let html = '';
+ for (let i = 0; i < totalLives; i++) {
+ html += i < lives ? 'โค๏ธ' : '๐ค';
+ }
+ return html;
+ }
+
+ // Get priority level string
+ function getPriorityLevel(priority) {
+ if (priority === 1) return 'high';
+ if (priority === 2) return 'medium';
+ return 'low';
+ }
+
// Show add habit modal (placeholder - full modal in next stories)
function showAddHabitModal() {
alert('Add Habit modal - coming in next story!');
}
+ // Show edit habit modal (placeholder)
+ function showEditHabitModal(habitId) {
+ alert('Edit Habit modal - coming in next story!');
+ }
+
+ // Delete habit (placeholder)
+ async function deleteHabit(habitId) {
+ if (!confirm('Are you sure you want to delete this habit?')) {
+ return;
+ }
+ alert('Delete functionality - coming in next story!');
+ }
+
+ // Check in habit (placeholder)
+ async function checkInHabit(habitId) {
+ alert('Check-in functionality - coming in next story!');
+ }
+
// Show error message
function showError(message) {
const container = document.getElementById('habitsContainer');
diff --git a/dashboard/tests/test_habits_frontend.py b/dashboard/tests/test_habits_frontend.py
index b4f7340..8766149 100644
--- a/dashboard/tests/test_habits_frontend.py
+++ b/dashboard/tests/test_habits_frontend.py
@@ -1,6 +1,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
"""
import sys
@@ -149,9 +150,168 @@ def test_typecheck():
print("โ Test 10: HTML structure is well-formed")
+def test_card_colored_border():
+ """Test 11: Habit card has colored left border matching habit.color"""
+ habits_path = Path(__file__).parent.parent / 'habits.html'
+ content = habits_path.read_text()
+
+ assert 'border-left-color' in content or 'borderLeftColor' in content, \
+ "Card should have colored left border"
+ assert 'habit.color' in content, "Card should use habit.color for border"
+ print("โ Test 11: Card has colored left border")
+
+def test_card_header_icons():
+ """Test 12: Card header shows icon, name, settings, and delete"""
+ habits_path = Path(__file__).parent.parent / 'habits.html'
+ content = habits_path.read_text()
+
+ # Check for icon display
+ assert 'habit.icon' in content or 'habit-card-icon' in content, \
+ "Card should display habit icon"
+
+ # Check for name display
+ assert 'habit.name' in content or 'habit-card-name' in content, \
+ "Card should display habit name"
+
+ # Check for settings (gear) icon
+ assert 'settings' in content.lower(), "Card should have settings icon"
+
+ # Check for delete (trash) icon
+ assert 'trash' in content.lower(), "Card should have delete icon"
+
+ print("โ Test 12: Card header has icon, name, settings, and delete")
+
+def test_card_streak_display():
+ """Test 13: Streak displays with fire emoji for current and trophy for best"""
+ habits_path = Path(__file__).parent.parent / 'habits.html'
+ content = habits_path.read_text()
+
+ assert '๐ฅ' in content, "Card should have fire emoji for current streak"
+ assert '๐' in content, "Card should have trophy emoji for best streak"
+ assert 'habit.streak' in content or 'streak?.current' in content or 'streak.current' in content, \
+ "Card should display streak.current"
+ assert 'streak?.best' in content or 'streak.best' in content, \
+ "Card should display streak.best"
+
+ print("โ Test 13: Streak display with fire and trophy emojis")
+
+def test_card_checkin_button():
+ """Test 14: Check-in button is large and centered"""
+ habits_path = Path(__file__).parent.parent / 'habits.html'
+ content = habits_path.read_text()
+
+ assert 'habit-card-check-btn' in content or 'check-btn' in content or 'checkin' in content.lower(), \
+ "Card should have check-in button"
+ assert 'Check In' in content or 'Check in' in content, \
+ "Button should have 'Check In' text"
+
+ # Check for button styling (large/centered)
+ assert 'width: 100%' in content or 'width:100%' in content, \
+ "Check-in button should be full-width"
+
+ print("โ Test 14: Check-in button is large and centered")
+
+def test_card_checkin_disabled_when_done():
+ """Test 15: Check-in button disabled when already checked today"""
+ habits_path = Path(__file__).parent.parent / 'habits.html'
+ content = habits_path.read_text()
+
+ assert 'disabled' in content, "Button should have disabled state"
+ assert 'Done today' in content or 'Done' in content, \
+ "Button should show 'Done today' when disabled"
+ assert 'isCheckedToday' in content or 'isDoneToday' in content, \
+ "Should have function to check if habit is done today"
+
+ print("โ Test 15: Check-in button disabled when done today")
+
+def test_card_lives_display():
+ """Test 16: Lives display shows filled and empty hearts (total 3)"""
+ habits_path = Path(__file__).parent.parent / 'habits.html'
+ content = habits_path.read_text()
+
+ assert 'โค๏ธ' in content or 'โฅ' in content, "Card should have filled heart emoji"
+ assert '๐ค' in content or 'โก' in content, "Card should have empty heart emoji"
+ assert 'habit.lives' in content or 'renderLives' in content, \
+ "Card should display lives"
+
+ # Check for lives rendering function
+ assert 'renderLives' in content or 'lives' in content.lower(), \
+ "Should have lives rendering logic"
+
+ print("โ Test 16: Lives display with hearts")
+
+def test_card_completion_rate():
+ """Test 17: Completion rate percentage is displayed"""
+ habits_path = Path(__file__).parent.parent / 'habits.html'
+ content = habits_path.read_text()
+
+ assert 'completion_rate' in content or 'completion' in content, \
+ "Card should display completion rate"
+ assert '(30d)' in content or '30d' in content, \
+ "Completion rate should show 30-day period"
+ assert '%' in content, "Completion rate should show percentage"
+
+ print("โ Test 17: Completion rate displayed")
+
+def test_card_footer_category_priority():
+ """Test 18: Footer shows category badge and priority"""
+ habits_path = Path(__file__).parent.parent / 'habits.html'
+ content = habits_path.read_text()
+
+ assert 'habit.category' in content or 'habit-card-category' in content, \
+ "Card should display category"
+ assert 'habit.priority' in content or 'priority' in content.lower(), \
+ "Card should display priority"
+ assert 'habit-card-footer' in content or 'footer' in content.lower(), \
+ "Card should have footer section"
+
+ print("โ Test 18: Footer shows category and priority")
+
+def test_card_lucide_createicons():
+ """Test 19: lucide.createIcons() is called after rendering cards"""
+ habits_path = Path(__file__).parent.parent / 'habits.html'
+ content = habits_path.read_text()
+
+ # Check that createIcons is called after rendering
+ render_pos = content.find('renderHabits')
+ if render_pos != -1:
+ after_render = content[render_pos:]
+ assert 'lucide.createIcons()' in after_render, \
+ "lucide.createIcons() should be called after rendering"
+
+ print("โ Test 19: lucide.createIcons() called after rendering")
+
+def test_card_common_css_variables():
+ """Test 20: Card uses common.css variables for styling"""
+ habits_path = Path(__file__).parent.parent / 'habits.html'
+ content = habits_path.read_text()
+
+ # Check for common.css variable usage
+ assert '--bg-surface' in content or '--text-primary' in content or '--border' in content, \
+ "Card should use common.css variables"
+ assert 'var(--' in content, "Should use CSS variables"
+
+ print("โ Test 20: Card uses common.css variables")
+
+def test_typecheck_us007():
+ """Test 21: Typecheck passes for US-007"""
+ habits_path = Path(__file__).parent.parent / 'habits.html'
+ assert habits_path.exists(), "habits.html should exist"
+
+ # Check that all functions are properly defined
+ content = habits_path.read_text()
+ assert 'function renderHabitCard(' in content, "renderHabitCard function should be defined"
+ assert 'function isCheckedToday(' in content, "isCheckedToday function should be defined"
+ assert 'function getLastCheckInfo(' in content, "getLastCheckInfo function should be defined"
+ assert 'function renderLives(' in content, "renderLives function should be defined"
+ assert 'function getPriorityLevel(' in content, "getPriorityLevel function should be defined"
+
+ print("โ Test 21: Typecheck passes (all functions defined)")
+
def run_all_tests():
"""Run all tests in sequence"""
tests = [
+ # US-006 tests
test_habits_html_exists,
test_habits_html_structure,
test_page_has_header,
@@ -162,9 +322,21 @@ def run_all_tests():
test_habit_card_rendering,
test_no_console_errors_structure,
test_typecheck,
+ # US-007 tests
+ test_card_colored_border,
+ test_card_header_icons,
+ test_card_streak_display,
+ test_card_checkin_button,
+ test_card_checkin_disabled_when_done,
+ test_card_lives_display,
+ test_card_completion_rate,
+ test_card_footer_category_priority,
+ test_card_lucide_createicons,
+ test_card_common_css_variables,
+ test_typecheck_us007,
]
- print(f"\nRunning {len(tests)} frontend tests for US-006...\n")
+ print(f"\nRunning {len(tests)} frontend tests for US-006 and US-007...\n")
failed = []
for test in tests: