feat: US-010 - Frontend - Check-in interaction (click and long-press)
This commit is contained in:
@@ -522,6 +522,74 @@
|
||||
.toast.error svg {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
/* Pulse animation */
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation: pulse 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
/* Check-in detail modal */
|
||||
.checkin-modal {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.rating-stars {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
justify-content: center;
|
||||
margin: var(--space-3) 0;
|
||||
}
|
||||
|
||||
.rating-star {
|
||||
font-size: 32px;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.rating-star.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.rating-star:hover {
|
||||
transform: scale(1.2);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.mood-buttons {
|
||||
display: flex;
|
||||
gap: var(--space-3);
|
||||
justify-content: center;
|
||||
margin: var(--space-3) 0;
|
||||
}
|
||||
|
||||
.mood-btn {
|
||||
font-size: 32px;
|
||||
cursor: pointer;
|
||||
padding: var(--space-2);
|
||||
border: 2px solid transparent;
|
||||
border-radius: var(--radius-md);
|
||||
transition: all var(--transition-base);
|
||||
background: none;
|
||||
}
|
||||
|
||||
.mood-btn:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.mood-btn.selected {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-muted);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -659,6 +727,53 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Check-in Detail Modal -->
|
||||
<div id="checkinModal" class="modal-overlay">
|
||||
<div class="modal checkin-modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Check-In Details</h2>
|
||||
<button class="modal-close" onclick="closeCheckinModal()">
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Note -->
|
||||
<div class="form-field">
|
||||
<label class="form-label" for="checkinNote">Note (optional)</label>
|
||||
<textarea id="checkinNote" class="form-textarea" placeholder="How did it go?"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Rating -->
|
||||
<div class="form-field">
|
||||
<label class="form-label">Rating (optional)</label>
|
||||
<div class="rating-stars" id="ratingStars">
|
||||
<span class="rating-star" data-rating="1" onclick="selectRating(1)">⭐</span>
|
||||
<span class="rating-star" data-rating="2" onclick="selectRating(2)">⭐</span>
|
||||
<span class="rating-star" data-rating="3" onclick="selectRating(3)">⭐</span>
|
||||
<span class="rating-star" data-rating="4" onclick="selectRating(4)">⭐</span>
|
||||
<span class="rating-star" data-rating="5" onclick="selectRating(5)">⭐</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mood -->
|
||||
<div class="form-field">
|
||||
<label class="form-label">Mood (optional)</label>
|
||||
<div class="mood-buttons">
|
||||
<button class="mood-btn" data-mood="happy" onclick="selectMood('happy')">😊</button>
|
||||
<button class="mood-btn" data-mood="neutral" onclick="selectMood('neutral')">😐</button>
|
||||
<button class="mood-btn" data-mood="sad" onclick="selectMood('sad')">😞</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn" onclick="closeCheckinModal()">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" id="submitCheckinBtn" onclick="submitCheckInDetail()">
|
||||
Done!
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Theme management
|
||||
function initTheme() {
|
||||
@@ -722,6 +837,26 @@
|
||||
const habitsHtml = habits.map(habit => renderHabitCard(habit)).join('');
|
||||
container.innerHTML = `<div class="habits-grid">${habitsHtml}</div>`;
|
||||
lucide.createIcons();
|
||||
|
||||
// Attach event handlers to check-in buttons
|
||||
habits.forEach(habit => {
|
||||
if (!isCheckedToday(habit)) {
|
||||
const btn = document.getElementById(`checkin-btn-${habit.id}`);
|
||||
if (btn) {
|
||||
// Right-click to open detail modal
|
||||
btn.addEventListener('contextmenu', (e) => handleCheckInButtonPress(habit.id, e, true));
|
||||
|
||||
// Mouse/touch events for long-press detection
|
||||
btn.addEventListener('mousedown', (e) => handleCheckInButtonPress(habit.id, e, true));
|
||||
btn.addEventListener('mouseup', (e) => handleCheckInButtonRelease(habit.id, e));
|
||||
btn.addEventListener('mouseleave', () => handleCheckInButtonCancel());
|
||||
|
||||
btn.addEventListener('touchstart', (e) => handleCheckInButtonPress(habit.id, e, false));
|
||||
btn.addEventListener('touchend', (e) => handleCheckInButtonRelease(habit.id, e));
|
||||
btn.addEventListener('touchcancel', () => handleCheckInButtonCancel());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Render single habit card
|
||||
@@ -757,7 +892,7 @@
|
||||
|
||||
<button
|
||||
class="habit-card-check-btn"
|
||||
onclick="checkInHabit('${habit.id}')"
|
||||
id="checkin-btn-${habit.id}"
|
||||
${isDoneToday ? 'disabled' : ''}
|
||||
>
|
||||
${isDoneToday ? '✓ Done today' : 'Check In'}
|
||||
@@ -897,6 +1032,11 @@
|
||||
if (e.target === modal) {
|
||||
closeHabitModal();
|
||||
}
|
||||
|
||||
const checkinModal = document.getElementById('checkinModal');
|
||||
if (e.target === checkinModal) {
|
||||
closeCheckinModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize color picker
|
||||
@@ -1201,9 +1341,209 @@
|
||||
alert('Delete functionality - coming in next story!');
|
||||
}
|
||||
|
||||
// Check in habit (placeholder)
|
||||
async function checkInHabit(habitId) {
|
||||
alert('Check-in functionality - coming in next story!');
|
||||
// Check-in state
|
||||
let checkInHabitId = null;
|
||||
let checkInRating = null;
|
||||
let checkInMood = null;
|
||||
let longPressTimer = null;
|
||||
|
||||
// Check in habit (simple click)
|
||||
async function checkInHabit(habitId, event) {
|
||||
// Prevent simple check-in if this was triggered during long-press detection
|
||||
if (event && event.type === 'mousedown') {
|
||||
return; // Let the long-press handler deal with it
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/echo/api/habits/${habitId}/check`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
|
||||
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 streak
|
||||
showToast(`Habit checked! 🔥 Streak: ${updatedHabit.streak?.current || 0}`, 'success');
|
||||
|
||||
// Refresh habits list
|
||||
await loadHabits();
|
||||
|
||||
// Add pulse animation to the updated card
|
||||
setTimeout(() => {
|
||||
const card = event?.target?.closest('.habit-card');
|
||||
if (card) {
|
||||
card.classList.add('pulse');
|
||||
setTimeout(() => card.classList.remove('pulse'), 500);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to check in:', error);
|
||||
showToast('Failed to check in: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Show check-in detail modal
|
||||
function showCheckInDetailModal(habitId) {
|
||||
checkInHabitId = habitId;
|
||||
checkInRating = null;
|
||||
checkInMood = null;
|
||||
|
||||
// Reset form
|
||||
document.getElementById('checkinNote').value = '';
|
||||
|
||||
// Reset rating stars
|
||||
document.querySelectorAll('.rating-star').forEach(star => {
|
||||
star.classList.remove('active');
|
||||
});
|
||||
|
||||
// Reset mood buttons
|
||||
document.querySelectorAll('.mood-btn').forEach(btn => {
|
||||
btn.classList.remove('selected');
|
||||
});
|
||||
|
||||
// Show modal
|
||||
const modal = document.getElementById('checkinModal');
|
||||
modal.classList.add('active');
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
// Close check-in detail modal
|
||||
function closeCheckinModal() {
|
||||
const modal = document.getElementById('checkinModal');
|
||||
modal.classList.remove('active');
|
||||
checkInHabitId = null;
|
||||
checkInRating = null;
|
||||
checkInMood = null;
|
||||
}
|
||||
|
||||
// Select rating
|
||||
function selectRating(rating) {
|
||||
checkInRating = rating;
|
||||
|
||||
// Update star display
|
||||
document.querySelectorAll('.rating-star').forEach((star, index) => {
|
||||
if (index < rating) {
|
||||
star.classList.add('active');
|
||||
} else {
|
||||
star.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Select mood
|
||||
function selectMood(mood) {
|
||||
checkInMood = mood;
|
||||
|
||||
// Update mood button display
|
||||
document.querySelectorAll('.mood-btn').forEach(btn => {
|
||||
if (btn.dataset.mood === mood) {
|
||||
btn.classList.add('selected');
|
||||
} else {
|
||||
btn.classList.remove('selected');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Submit check-in with details
|
||||
async function submitCheckInDetail() {
|
||||
if (!checkInHabitId) {
|
||||
showToast('No habit selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const submitBtn = document.getElementById('submitCheckinBtn');
|
||||
submitBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const note = document.getElementById('checkinNote').value.trim();
|
||||
|
||||
// Build request body with optional fields
|
||||
const body = {};
|
||||
if (note) body.note = note;
|
||||
if (checkInRating) body.rating = checkInRating;
|
||||
if (checkInMood) body.mood = checkInMood;
|
||||
|
||||
const response = await fetch(`/echo/api/habits/${checkInHabitId}/check`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
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 streak
|
||||
showToast(`Habit checked! 🔥 Streak: ${updatedHabit.streak?.current || 0}`, 'success');
|
||||
|
||||
// Close modal
|
||||
closeCheckinModal();
|
||||
|
||||
// Refresh habits list
|
||||
await loadHabits();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to check in:', error);
|
||||
showToast('Failed to check in: ' + error.message, 'error');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle long-press for check-in detail modal
|
||||
function handleCheckInButtonPress(habitId, event, isMouseEvent) {
|
||||
// Prevent default context menu on right-click
|
||||
if (event.type === 'contextmenu') {
|
||||
event.preventDefault();
|
||||
showCheckInDetailModal(habitId);
|
||||
return;
|
||||
}
|
||||
|
||||
// For touch/mouse press, start long-press timer
|
||||
if (event.type === 'mousedown' || event.type === 'touchstart') {
|
||||
event.preventDefault();
|
||||
|
||||
longPressTimer = setTimeout(() => {
|
||||
showCheckInDetailModal(habitId);
|
||||
longPressTimer = null;
|
||||
}, 500); // 500ms for long-press
|
||||
}
|
||||
}
|
||||
|
||||
// Handle release of check-in button
|
||||
function handleCheckInButtonRelease(habitId, event) {
|
||||
if (event.type === 'mouseup' || event.type === 'touchend') {
|
||||
// If long-press timer is still running, it was a short press
|
||||
if (longPressTimer) {
|
||||
clearTimeout(longPressTimer);
|
||||
longPressTimer = null;
|
||||
|
||||
// Perform simple check-in
|
||||
checkInHabit(habitId, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel long-press if moved away
|
||||
function handleCheckInButtonCancel() {
|
||||
if (longPressTimer) {
|
||||
clearTimeout(longPressTimer);
|
||||
longPressTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Show error message
|
||||
|
||||
@@ -4,6 +4,7 @@ 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
|
||||
Story US-009: Frontend - Edit habit modal
|
||||
Story US-010: Frontend - Check-in interaction (click and long-press)
|
||||
"""
|
||||
|
||||
import sys
|
||||
@@ -748,6 +749,253 @@ def test_typecheck_us009():
|
||||
|
||||
print("✓ Test 44: Typecheck passes (edit modal fully implemented)")
|
||||
|
||||
def test_checkin_simple_click():
|
||||
"""Test 45: Simple click on check-in button sends POST request"""
|
||||
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||
content = habits_path.read_text()
|
||||
|
||||
# Check that checkInHabit function exists and does POST
|
||||
assert 'function checkInHabit' in content or 'async function checkInHabit' in content, \
|
||||
"checkInHabit function should be defined"
|
||||
|
||||
checkin_start = content.find('function checkInHabit')
|
||||
checkin_end = content.find('\n }', checkin_start + 500)
|
||||
checkin_func = content[checkin_start:checkin_end]
|
||||
|
||||
assert "fetch(`/echo/api/habits/${habitId}/check`" in checkin_func or \
|
||||
'fetch(`/echo/api/habits/${habitId}/check`' in checkin_func, \
|
||||
"Should POST to /echo/api/habits/{id}/check"
|
||||
assert "method: 'POST'" in checkin_func, "Should use POST method"
|
||||
|
||||
print("✓ Test 45: Simple click sends POST to check-in endpoint")
|
||||
|
||||
def test_checkin_detail_modal_structure():
|
||||
"""Test 46: Check-in detail modal exists with note, rating, and mood fields"""
|
||||
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||
content = habits_path.read_text()
|
||||
|
||||
# Check modal exists
|
||||
assert 'id="checkinModal"' in content, "Should have check-in detail modal"
|
||||
assert 'Check-In Details' in content or 'Check-in Details' in content, \
|
||||
"Modal should have title 'Check-In Details'"
|
||||
|
||||
# Check for note textarea
|
||||
assert 'id="checkinNote"' in content, "Should have note textarea"
|
||||
assert '<textarea' in content, "Should have textarea element"
|
||||
|
||||
# Check for rating stars
|
||||
assert 'rating-star' in content, "Should have rating star elements"
|
||||
assert 'selectRating' in content, "Should have selectRating function"
|
||||
|
||||
# Check for mood buttons
|
||||
assert 'mood-btn' in content, "Should have mood button elements"
|
||||
assert 'selectMood' in content, "Should have selectMood function"
|
||||
assert '😊' in content and '😐' in content and '😞' in content, \
|
||||
"Should have happy, neutral, and sad emojis"
|
||||
|
||||
print("✓ Test 46: Check-in detail modal has note, rating, and mood fields")
|
||||
|
||||
def test_checkin_long_press_handler():
|
||||
"""Test 47: Long-press (mobile) and right-click (desktop) handlers exist"""
|
||||
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||
content = habits_path.read_text()
|
||||
|
||||
# Check for long-press handling functions
|
||||
assert 'handleCheckInButtonPress' in content, \
|
||||
"Should have handleCheckInButtonPress function"
|
||||
assert 'handleCheckInButtonRelease' in content, \
|
||||
"Should have handleCheckInButtonRelease function"
|
||||
|
||||
# Check for contextmenu event (right-click)
|
||||
assert "contextmenu" in content, "Should handle contextmenu event for right-click"
|
||||
|
||||
# Check for touch/mouse events
|
||||
assert "mousedown" in content, "Should handle mousedown event"
|
||||
assert "mouseup" in content, "Should handle mouseup event"
|
||||
assert "touchstart" in content or "touch" in content, \
|
||||
"Should handle touch events for mobile"
|
||||
|
||||
# Check for long-press timer
|
||||
assert 'longPressTimer' in content, "Should track long-press timer"
|
||||
assert '500' in content, "Should use 500ms delay for long-press"
|
||||
|
||||
print("✓ Test 47: Long-press and right-click handlers exist")
|
||||
|
||||
def test_checkin_detail_modal_functions():
|
||||
"""Test 48: Modal functions (show, close, selectRating, selectMood) are defined"""
|
||||
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||
content = habits_path.read_text()
|
||||
|
||||
# Check modal control functions
|
||||
assert 'function showCheckInDetailModal' in content, \
|
||||
"Should have showCheckInDetailModal function"
|
||||
assert 'function closeCheckinModal' in content, \
|
||||
"Should have closeCheckinModal function"
|
||||
|
||||
# Check selection functions
|
||||
assert 'function selectRating' in content, "Should have selectRating function"
|
||||
assert 'function selectMood' in content, "Should have selectMood function"
|
||||
|
||||
# Check submit function
|
||||
assert 'function submitCheckInDetail' in content or 'async function submitCheckInDetail' in content, \
|
||||
"Should have submitCheckInDetail function"
|
||||
|
||||
print("✓ Test 48: Check-in modal functions are defined")
|
||||
|
||||
def test_checkin_detail_submit():
|
||||
"""Test 49: Detail modal submit sends note, rating, and mood"""
|
||||
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||
content = habits_path.read_text()
|
||||
|
||||
# Find submitCheckInDetail function
|
||||
submit_start = content.find('function submitCheckInDetail')
|
||||
submit_end = content.find('\n }', submit_start + 1000)
|
||||
submit_func = content[submit_start:submit_end]
|
||||
|
||||
# Check it builds body with optional fields
|
||||
assert 'body.note' in submit_func or 'if (note)' in submit_func, \
|
||||
"Should add note to body if provided"
|
||||
assert 'body.rating' in submit_func or 'checkInRating' in submit_func, \
|
||||
"Should add rating to body if provided"
|
||||
assert 'body.mood' in submit_func or 'checkInMood' in submit_func, \
|
||||
"Should add mood to body if provided"
|
||||
|
||||
# Check it POSTs to API
|
||||
assert "fetch(`/echo/api/habits/" in submit_func, "Should POST to API"
|
||||
assert "/check`" in submit_func, "Should POST to check endpoint"
|
||||
|
||||
print("✓ Test 49: Detail modal submit includes optional fields")
|
||||
|
||||
def test_checkin_toast_with_streak():
|
||||
"""Test 50: Toast notification shows 'Habit checked! 🔥 Streak: X'"""
|
||||
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||
content = habits_path.read_text()
|
||||
|
||||
# Check for toast with streak message
|
||||
assert 'Habit checked!' in content, "Should show 'Habit checked!' message"
|
||||
assert '🔥' in content, "Should include fire emoji in streak message"
|
||||
assert 'Streak:' in content, "Should show streak count"
|
||||
|
||||
# Check that streak value comes from API response
|
||||
checkin_start = content.find('function checkInHabit')
|
||||
checkin_area = content[checkin_start:checkin_start+2000]
|
||||
|
||||
assert 'updatedHabit' in checkin_area or 'await response.json()' in checkin_area, \
|
||||
"Should get updated habit from response"
|
||||
assert 'streak' in checkin_area, "Should access streak from response"
|
||||
|
||||
print("✓ Test 50: Toast shows 'Habit checked! 🔥 Streak: X'")
|
||||
|
||||
def test_checkin_button_disabled_after():
|
||||
"""Test 51: Check-in button becomes disabled with 'Done today' after check-in"""
|
||||
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||
content = habits_path.read_text()
|
||||
|
||||
# Check that renderHabitCard uses isCheckedToday to disable button
|
||||
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 text changes
|
||||
assert '✓ Done today' in content or 'Done today' in content, \
|
||||
"Button should show 'Done today' when disabled"
|
||||
assert 'Check In' in content, "Button should show 'Check In' when enabled"
|
||||
|
||||
print("✓ Test 51: Button becomes disabled with 'Done today' after check-in")
|
||||
|
||||
def test_checkin_pulse_animation():
|
||||
"""Test 52: Pulse animation plays on card after successful check-in"""
|
||||
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||
content = habits_path.read_text()
|
||||
|
||||
# Check for pulse animation CSS
|
||||
assert '@keyframes pulse' in content, "Should define pulse animation"
|
||||
assert 'transform: scale' in content, "Pulse should use scale transform"
|
||||
assert '.pulse' in content, "Should have pulse CSS class"
|
||||
|
||||
# Check that checkInHabit adds pulse class
|
||||
checkin_start = content.find('function checkInHabit')
|
||||
checkin_area = content[checkin_start:checkin_start+2000]
|
||||
|
||||
assert 'pulse' in checkin_area or 'classList.add' in checkin_area, \
|
||||
"Should add pulse class to card after check-in"
|
||||
|
||||
print("✓ Test 52: Pulse animation defined and applied")
|
||||
|
||||
def test_checkin_prevent_context_menu():
|
||||
"""Test 53: Right-click prevents default context menu"""
|
||||
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||
content = habits_path.read_text()
|
||||
|
||||
# Check for preventDefault on contextmenu
|
||||
press_handler_start = content.find('function handleCheckInButtonPress')
|
||||
press_handler_end = content.find('\n }', press_handler_start + 1000)
|
||||
press_handler = content[press_handler_start:press_handler_end]
|
||||
|
||||
assert 'contextmenu' in press_handler, "Should check for contextmenu event"
|
||||
assert 'preventDefault()' in press_handler, "Should call preventDefault on right-click"
|
||||
|
||||
print("✓ Test 53: Right-click prevents default context menu")
|
||||
|
||||
def test_checkin_event_listeners_attached():
|
||||
"""Test 54: Event listeners are attached to check-in buttons in renderHabits"""
|
||||
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||
content = habits_path.read_text()
|
||||
|
||||
# Find renderHabits function
|
||||
render_start = content.find('function renderHabits()')
|
||||
render_end = content.find('\n }', render_start + 2000)
|
||||
render_func = content[render_start:render_end]
|
||||
|
||||
# Check that event listeners are attached
|
||||
assert 'addEventListener' in render_func, \
|
||||
"Should attach event listeners after rendering"
|
||||
assert 'contextmenu' in render_func or 'contextmenu' in content[render_end:render_end+1000], \
|
||||
"Should attach contextmenu listener"
|
||||
assert 'mousedown' in render_func or 'mousedown' in content[render_end:render_end+1000], \
|
||||
"Should attach mousedown listener"
|
||||
|
||||
# Check that we iterate through habits to attach listeners
|
||||
assert 'habits.forEach' in render_func or 'for' in render_func, \
|
||||
"Should iterate through habits to attach listeners"
|
||||
|
||||
print("✓ Test 54: Event listeners attached in renderHabits")
|
||||
|
||||
def test_typecheck_us010():
|
||||
"""Test 55: Typecheck passes - all check-in functions defined"""
|
||||
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||
content = habits_path.read_text()
|
||||
|
||||
# Check all required functions are defined
|
||||
required_functions = [
|
||||
'checkInHabit',
|
||||
'showCheckInDetailModal',
|
||||
'closeCheckinModal',
|
||||
'selectRating',
|
||||
'selectMood',
|
||||
'submitCheckInDetail',
|
||||
'handleCheckInButtonPress',
|
||||
'handleCheckInButtonRelease',
|
||||
'handleCheckInButtonCancel'
|
||||
]
|
||||
|
||||
for func in required_functions:
|
||||
assert f'function {func}' in content or f'async function {func}' in content, \
|
||||
f"{func} should be defined"
|
||||
|
||||
# Check state variables
|
||||
assert 'let checkInHabitId' in content, "checkInHabitId should be declared"
|
||||
assert 'let checkInRating' in content, "checkInRating should be declared"
|
||||
assert 'let checkInMood' in content, "checkInMood should be declared"
|
||||
assert 'let longPressTimer' in content, "longPressTimer should be declared"
|
||||
|
||||
print("✓ Test 55: Typecheck passes (all check-in functions and variables defined)")
|
||||
|
||||
def run_all_tests():
|
||||
"""Run all tests in sequence"""
|
||||
tests = [
|
||||
@@ -799,9 +1047,21 @@ def run_all_tests():
|
||||
test_edit_modal_close_resets_state,
|
||||
test_edit_modal_no_console_errors,
|
||||
test_typecheck_us009,
|
||||
# US-010 tests
|
||||
test_checkin_simple_click,
|
||||
test_checkin_detail_modal_structure,
|
||||
test_checkin_long_press_handler,
|
||||
test_checkin_detail_modal_functions,
|
||||
test_checkin_detail_submit,
|
||||
test_checkin_toast_with_streak,
|
||||
test_checkin_button_disabled_after,
|
||||
test_checkin_pulse_animation,
|
||||
test_checkin_prevent_context_menu,
|
||||
test_checkin_event_listeners_attached,
|
||||
test_typecheck_us010,
|
||||
]
|
||||
|
||||
print(f"\nRunning {len(tests)} frontend tests for US-006, US-007, US-008, and US-009...\n")
|
||||
print(f"\nRunning {len(tests)} frontend tests for US-006, US-007, US-008, US-009, and US-010...\n")
|
||||
|
||||
failed = []
|
||||
for test in tests:
|
||||
|
||||
Reference in New Issue
Block a user