feat: 8.0 - Frontend - Create habit form modal
This commit is contained in:
@@ -83,6 +83,121 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
.modal-overlay {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 1000;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
background: var(--bg-base);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--space-5);
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: var(--space-2);
|
||||||
|
margin-top: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Radio group */
|
||||||
|
.radio-group {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-option {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-option input[type="radio"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--space-3);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-option input[type="radio"]:checked + .radio-label {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-label:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast */
|
||||||
|
.toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: var(--space-5);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) translateY(100px);
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: var(--space-3) var(--space-5);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.show {
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -143,6 +258,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- Add Habit Modal -->
|
||||||
|
<div class="modal-overlay" id="habitModal">
|
||||||
|
<div class="modal">
|
||||||
|
<h2 class="modal-title">Obișnuință nouă</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Nume *</label>
|
||||||
|
<input type="text" class="input" id="habitName" placeholder="ex: Bazin, Sală, Meditație...">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Frecvență</label>
|
||||||
|
<div class="radio-group">
|
||||||
|
<div class="radio-option">
|
||||||
|
<input type="radio" name="frequency" id="freqDaily" value="daily" checked>
|
||||||
|
<label for="freqDaily" class="radio-label">Zilnic</label>
|
||||||
|
</div>
|
||||||
|
<div class="radio-option">
|
||||||
|
<input type="radio" name="frequency" id="freqWeekly" value="weekly">
|
||||||
|
<label for="freqWeekly" class="radio-label">Săptămânal</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn btn-secondary" onclick="hideHabitModal()">Anulează</button>
|
||||||
|
<button class="btn btn-primary" id="habitCreateBtn" onclick="createHabit()" disabled>Creează</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toast" id="toast"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Theme management
|
// Theme management
|
||||||
function initTheme() {
|
function initTheme() {
|
||||||
@@ -171,14 +316,90 @@
|
|||||||
initTheme();
|
initTheme();
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
|
|
||||||
// Placeholder function for add habit modal (to be implemented in future stories)
|
// Modal functions
|
||||||
function showAddHabitModal() {
|
function showAddHabitModal() {
|
||||||
alert('Funcționalitatea de adăugare obișnuință va fi implementată în următoarea fază!');
|
const modal = document.getElementById('habitModal');
|
||||||
|
const nameInput = document.getElementById('habitName');
|
||||||
|
const createBtn = document.getElementById('habitCreateBtn');
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
nameInput.value = '';
|
||||||
|
document.getElementById('freqDaily').checked = true;
|
||||||
|
createBtn.disabled = true;
|
||||||
|
|
||||||
|
// Show modal
|
||||||
|
modal.classList.add('active');
|
||||||
|
nameInput.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideHabitModal() {
|
||||||
|
const modal = document.getElementById('habitModal');
|
||||||
|
modal.classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form validation - enable/disable Create button based on name input
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const nameInput = document.getElementById('habitName');
|
||||||
|
const createBtn = document.getElementById('habitCreateBtn');
|
||||||
|
|
||||||
|
nameInput.addEventListener('input', () => {
|
||||||
|
const name = nameInput.value.trim();
|
||||||
|
createBtn.disabled = name.length === 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Allow Enter key to submit if button is enabled
|
||||||
|
nameInput.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter' && !createBtn.disabled) {
|
||||||
|
createHabit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create habit
|
||||||
|
async function createHabit() {
|
||||||
|
const nameInput = document.getElementById('habitName');
|
||||||
|
const name = nameInput.value.trim();
|
||||||
|
const frequency = document.querySelector('input[name="frequency"]:checked').value;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
showToast('Te rog introdu un nume pentru obișnuință');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/habits', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name, frequency })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
hideHabitModal();
|
||||||
|
showToast('Obișnuință creată cu succes!');
|
||||||
|
loadHabits();
|
||||||
|
} else {
|
||||||
|
const error = await response.text();
|
||||||
|
showToast('Eroare la crearea obișnuinței: ' + error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showToast('Eroare la conectarea cu serverul');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toast notification
|
||||||
|
function showToast(message) {
|
||||||
|
const toast = document.getElementById('toast');
|
||||||
|
toast.textContent = message;
|
||||||
|
toast.classList.add('show');
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.remove('show');
|
||||||
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load habits (placeholder for future API integration)
|
// Load habits (placeholder for future API integration)
|
||||||
async function loadHabits() {
|
async function loadHabits() {
|
||||||
// Will be implemented when API is integrated
|
// Will be implemented in next story
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
342
dashboard/test_habits_modal.py
Normal file
342
dashboard/test_habits_modal.py
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Tests for habit creation modal in habits.html
|
||||||
|
Validates modal structure, form elements, buttons, and styling
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from html.parser import HTMLParser
|
||||||
|
|
||||||
|
HABITS_HTML_PATH = 'dashboard/habits.html'
|
||||||
|
|
||||||
|
class ModalParser(HTMLParser):
|
||||||
|
"""Parser to extract modal elements from HTML"""
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.in_modal = False
|
||||||
|
self.in_modal_title = False
|
||||||
|
self.in_form_label = False
|
||||||
|
self.in_button = False
|
||||||
|
self.modal_title = None
|
||||||
|
self.form_labels = []
|
||||||
|
self.name_input_attrs = None
|
||||||
|
self.radio_buttons = []
|
||||||
|
self.buttons = []
|
||||||
|
self.radio_labels = []
|
||||||
|
self.in_radio_label = False
|
||||||
|
self.current_radio_label = None
|
||||||
|
self.modal_overlay_found = False
|
||||||
|
self.modal_div_found = False
|
||||||
|
self.toast_found = False
|
||||||
|
|
||||||
|
def handle_starttag(self, tag, attrs):
|
||||||
|
attrs_dict = dict(attrs)
|
||||||
|
|
||||||
|
# Check for modal overlay
|
||||||
|
if tag == 'div' and attrs_dict.get('id') == 'habitModal':
|
||||||
|
self.modal_overlay_found = True
|
||||||
|
if 'modal-overlay' in attrs_dict.get('class', ''):
|
||||||
|
self.in_modal = True
|
||||||
|
|
||||||
|
# Check for modal div
|
||||||
|
if self.in_modal and tag == 'div' and 'modal' in attrs_dict.get('class', ''):
|
||||||
|
self.modal_div_found = True
|
||||||
|
|
||||||
|
# Check for modal title
|
||||||
|
if self.in_modal and tag == 'h2' and 'modal-title' in attrs_dict.get('class', ''):
|
||||||
|
self.in_modal_title = True
|
||||||
|
|
||||||
|
# Check for form labels
|
||||||
|
if self.in_modal and tag == 'label' and 'form-label' in attrs_dict.get('class', ''):
|
||||||
|
self.in_form_label = True
|
||||||
|
|
||||||
|
# Check for name input
|
||||||
|
if self.in_modal and tag == 'input' and attrs_dict.get('id') == 'habitName':
|
||||||
|
self.name_input_attrs = attrs_dict
|
||||||
|
|
||||||
|
# Check for radio buttons
|
||||||
|
if self.in_modal and tag == 'input' and attrs_dict.get('type') == 'radio':
|
||||||
|
self.radio_buttons.append(attrs_dict)
|
||||||
|
|
||||||
|
# Check for radio labels
|
||||||
|
if self.in_modal and tag == 'label' and 'radio-label' in attrs_dict.get('class', ''):
|
||||||
|
self.in_radio_label = True
|
||||||
|
self.current_radio_label = attrs_dict.get('for', '')
|
||||||
|
|
||||||
|
# Check for buttons
|
||||||
|
if self.in_modal and tag == 'button':
|
||||||
|
self.buttons.append(attrs_dict)
|
||||||
|
self.in_button = True
|
||||||
|
|
||||||
|
# Check for toast
|
||||||
|
if tag == 'div' and attrs_dict.get('id') == 'toast':
|
||||||
|
self.toast_found = True
|
||||||
|
|
||||||
|
def handle_endtag(self, tag):
|
||||||
|
if tag == 'h2':
|
||||||
|
self.in_modal_title = False
|
||||||
|
if tag == 'label':
|
||||||
|
self.in_form_label = False
|
||||||
|
self.in_radio_label = False
|
||||||
|
if tag == 'button':
|
||||||
|
self.in_button = False
|
||||||
|
if tag == 'div' and self.in_modal:
|
||||||
|
# Don't close modal state until we're sure we've left it
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handle_data(self, data):
|
||||||
|
if self.in_modal_title and not self.modal_title:
|
||||||
|
self.modal_title = data.strip()
|
||||||
|
if self.in_form_label:
|
||||||
|
self.form_labels.append(data.strip())
|
||||||
|
if self.in_radio_label:
|
||||||
|
self.radio_labels.append({'for': self.current_radio_label, 'text': data.strip()})
|
||||||
|
|
||||||
|
def test_modal_structure():
|
||||||
|
"""Test modal HTML structure exists"""
|
||||||
|
with open(HABITS_HTML_PATH, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
parser = ModalParser()
|
||||||
|
parser.feed(content)
|
||||||
|
|
||||||
|
# Check modal overlay exists
|
||||||
|
assert parser.modal_overlay_found, "Modal overlay with id='habitModal' not found"
|
||||||
|
|
||||||
|
# Check modal container exists
|
||||||
|
assert parser.modal_div_found, "Modal div with class='modal' not found"
|
||||||
|
|
||||||
|
# Check modal title
|
||||||
|
assert parser.modal_title is not None, "Modal title not found"
|
||||||
|
assert 'nou' in parser.modal_title.lower(), f"Modal title should mention 'nou', got: {parser.modal_title}"
|
||||||
|
|
||||||
|
print("✓ Modal structure exists")
|
||||||
|
|
||||||
|
def test_name_input_field():
|
||||||
|
"""Test habit name input field exists and is required"""
|
||||||
|
with open(HABITS_HTML_PATH, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
parser = ModalParser()
|
||||||
|
parser.feed(content)
|
||||||
|
|
||||||
|
# Find name input
|
||||||
|
assert parser.name_input_attrs is not None, "Name input field with id='habitName' not found"
|
||||||
|
|
||||||
|
# Check it's a text input
|
||||||
|
assert parser.name_input_attrs.get('type') == 'text', "Name input should be type='text'"
|
||||||
|
|
||||||
|
# Check it has class 'input'
|
||||||
|
assert 'input' in parser.name_input_attrs.get('class', ''), "Name input should have class='input'"
|
||||||
|
|
||||||
|
# Check it has placeholder
|
||||||
|
assert parser.name_input_attrs.get('placeholder'), "Name input should have placeholder"
|
||||||
|
|
||||||
|
# Check label exists and mentions required (*)
|
||||||
|
found_required_label = any('*' in label for label in parser.form_labels)
|
||||||
|
assert found_required_label, "Should have a form label with * indicating required field"
|
||||||
|
|
||||||
|
print("✓ Name input field exists with required indicator")
|
||||||
|
|
||||||
|
def test_frequency_radio_buttons():
|
||||||
|
"""Test frequency radio buttons exist with daily and weekly options"""
|
||||||
|
with open(HABITS_HTML_PATH, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
parser = ModalParser()
|
||||||
|
parser.feed(content)
|
||||||
|
|
||||||
|
# Check we have radio buttons
|
||||||
|
assert len(parser.radio_buttons) >= 2, f"Should have at least 2 radio buttons, found {len(parser.radio_buttons)}"
|
||||||
|
|
||||||
|
# Find daily radio button
|
||||||
|
daily_radio = next((r for r in parser.radio_buttons if r.get('value') == 'daily'), None)
|
||||||
|
assert daily_radio is not None, "Daily radio button with value='daily' not found"
|
||||||
|
assert daily_radio.get('name') == 'frequency', "Daily radio should have name='frequency'"
|
||||||
|
# Check for 'checked' attribute - it may be None or empty string when present
|
||||||
|
assert 'checked' in daily_radio, "Daily radio should be checked by default"
|
||||||
|
|
||||||
|
# Find weekly radio button
|
||||||
|
weekly_radio = next((r for r in parser.radio_buttons if r.get('value') == 'weekly'), None)
|
||||||
|
assert weekly_radio is not None, "Weekly radio button with value='weekly' not found"
|
||||||
|
assert weekly_radio.get('name') == 'frequency', "Weekly radio should have name='frequency'"
|
||||||
|
|
||||||
|
# Check labels exist with Romanian text
|
||||||
|
daily_label = next((l for l in parser.radio_labels if 'zilnic' in l['text'].lower()), None)
|
||||||
|
assert daily_label is not None, "Daily label with 'Zilnic' text not found"
|
||||||
|
|
||||||
|
weekly_label = next((l for l in parser.radio_labels if 'săptămânal' in l['text'].lower()), None)
|
||||||
|
assert weekly_label is not None, "Weekly label with 'Săptămânal' text not found"
|
||||||
|
|
||||||
|
print("✓ Frequency radio buttons exist with daily (default) and weekly options")
|
||||||
|
|
||||||
|
def test_modal_buttons():
|
||||||
|
"""Test modal has Cancel and Create buttons"""
|
||||||
|
with open(HABITS_HTML_PATH, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
parser = ModalParser()
|
||||||
|
parser.feed(content)
|
||||||
|
|
||||||
|
# Check we have 2 buttons
|
||||||
|
assert len(parser.buttons) >= 2, f"Should have at least 2 buttons, found {len(parser.buttons)}"
|
||||||
|
|
||||||
|
# Check Cancel button
|
||||||
|
cancel_btn = next((b for b in parser.buttons if 'btn-secondary' in b.get('class', '')), None)
|
||||||
|
assert cancel_btn is not None, "Cancel button with class='btn-secondary' not found"
|
||||||
|
assert 'hideHabitModal' in cancel_btn.get('onclick', ''), "Cancel should call hideHabitModal"
|
||||||
|
|
||||||
|
# Check Create button
|
||||||
|
create_btn = next((b for b in parser.buttons if b.get('id') == 'habitCreateBtn'), None)
|
||||||
|
assert create_btn is not None, "Create button with id='habitCreateBtn' not found"
|
||||||
|
assert 'btn-primary' in create_btn.get('class', ''), "Create button should have class='btn-primary'"
|
||||||
|
assert 'createHabit' in create_btn.get('onclick', ''), "Create should call createHabit"
|
||||||
|
# Check for 'disabled' attribute - it may be None or empty string when present
|
||||||
|
assert 'disabled' in create_btn, "Create button should start disabled"
|
||||||
|
|
||||||
|
# Check button text in content
|
||||||
|
assert 'anulează' in content.lower(), "Cancel button should say 'Anulează'"
|
||||||
|
assert 'creează' in content.lower(), "Create button should say 'Creează'"
|
||||||
|
|
||||||
|
print("✓ Modal has Cancel and Create buttons with correct attributes")
|
||||||
|
|
||||||
|
def test_add_button_triggers_modal():
|
||||||
|
"""Test that add habit button calls showAddHabitModal"""
|
||||||
|
with open(HABITS_HTML_PATH, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Find add habit button
|
||||||
|
assert 'class="add-habit-btn"' in content, "Add habit button not found"
|
||||||
|
assert 'showAddHabitModal()' in content, "Add button should call showAddHabitModal()"
|
||||||
|
|
||||||
|
print("✓ Add habit button calls showAddHabitModal()")
|
||||||
|
|
||||||
|
def test_modal_styling():
|
||||||
|
"""Test modal uses dashboard modal styling patterns"""
|
||||||
|
with open(HABITS_HTML_PATH, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Check key modal classes exist in CSS
|
||||||
|
required_styles = [
|
||||||
|
'.modal-overlay',
|
||||||
|
'.modal-overlay.active',
|
||||||
|
'.modal {',
|
||||||
|
'.modal-title',
|
||||||
|
'.modal-actions',
|
||||||
|
'.form-group',
|
||||||
|
'.form-label',
|
||||||
|
'.radio-group',
|
||||||
|
]
|
||||||
|
|
||||||
|
for style in required_styles:
|
||||||
|
assert style in content, f"Modal style '{style}' not found"
|
||||||
|
|
||||||
|
# Check modal uses CSS variables (dashboard pattern)
|
||||||
|
assert 'var(--bg-base)' in content, "Modal should use --bg-base"
|
||||||
|
assert 'var(--border)' in content, "Modal should use --border"
|
||||||
|
assert 'var(--accent)' in content, "Modal should use --accent"
|
||||||
|
|
||||||
|
print("✓ Modal uses dashboard modal styling patterns")
|
||||||
|
|
||||||
|
def test_javascript_functions():
|
||||||
|
"""Test JavaScript functions for modal interaction exist"""
|
||||||
|
with open(HABITS_HTML_PATH, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Check essential functions exist
|
||||||
|
assert 'function showAddHabitModal()' in content, "showAddHabitModal function not found"
|
||||||
|
assert 'function hideHabitModal()' in content, "hideHabitModal function not found"
|
||||||
|
assert 'async function createHabit()' in content or 'function createHabit()' in content, "createHabit function not found"
|
||||||
|
|
||||||
|
# Check form validation logic
|
||||||
|
assert "createBtn.disabled" in content, "Create button disable logic not found"
|
||||||
|
assert "nameInput.value.trim()" in content, "Name trim validation not found"
|
||||||
|
|
||||||
|
# Check modal show/hide logic
|
||||||
|
assert "modal.classList.add('active')" in content, "Modal show logic not found"
|
||||||
|
assert "modal.classList.remove('active')" in content, "Modal hide logic not found"
|
||||||
|
|
||||||
|
# Check API integration
|
||||||
|
assert "fetch('/api/habits'" in content, "API call to /api/habits not found"
|
||||||
|
assert "method: 'POST'" in content, "POST method not found"
|
||||||
|
|
||||||
|
print("✓ JavaScript functions for modal interaction exist")
|
||||||
|
|
||||||
|
def test_toast_notification():
|
||||||
|
"""Test toast notification element exists"""
|
||||||
|
with open(HABITS_HTML_PATH, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
parser = ModalParser()
|
||||||
|
parser.feed(content)
|
||||||
|
|
||||||
|
# Find toast element
|
||||||
|
assert parser.toast_found, "Toast notification element with id='toast' not found"
|
||||||
|
|
||||||
|
# Check toast styles exist
|
||||||
|
assert '.toast' in content, "Toast styles not found"
|
||||||
|
assert '.toast.show' in content, "Toast show state styles not found"
|
||||||
|
|
||||||
|
# Check showToast function exists
|
||||||
|
assert 'function showToast(' in content, "showToast function not found"
|
||||||
|
|
||||||
|
print("✓ Toast notification element exists")
|
||||||
|
|
||||||
|
def test_form_validation_event_listeners():
|
||||||
|
"""Test form validation with event listeners"""
|
||||||
|
with open(HABITS_HTML_PATH, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Check for DOMContentLoaded event listener
|
||||||
|
assert "addEventListener('DOMContentLoaded'" in content or "DOMContentLoaded" in content, \
|
||||||
|
"Should have DOMContentLoaded event listener"
|
||||||
|
|
||||||
|
# Check for input event listener on name field
|
||||||
|
assert "addEventListener('input'" in content, "Should have input event listener for validation"
|
||||||
|
|
||||||
|
# Check for Enter key handling
|
||||||
|
assert "addEventListener('keypress'" in content or "e.key === 'Enter'" in content, \
|
||||||
|
"Should handle Enter key submission"
|
||||||
|
|
||||||
|
print("✓ Form validation event listeners exist")
|
||||||
|
|
||||||
|
def run_tests():
|
||||||
|
"""Run all tests"""
|
||||||
|
tests = [
|
||||||
|
test_modal_structure,
|
||||||
|
test_name_input_field,
|
||||||
|
test_frequency_radio_buttons,
|
||||||
|
test_modal_buttons,
|
||||||
|
test_add_button_triggers_modal,
|
||||||
|
test_modal_styling,
|
||||||
|
test_javascript_functions,
|
||||||
|
test_toast_notification,
|
||||||
|
test_form_validation_event_listeners,
|
||||||
|
]
|
||||||
|
|
||||||
|
print("Running habit modal tests...\n")
|
||||||
|
|
||||||
|
failed = []
|
||||||
|
for test in tests:
|
||||||
|
try:
|
||||||
|
test()
|
||||||
|
except AssertionError as e:
|
||||||
|
print(f"✗ {test.__name__}: {e}")
|
||||||
|
failed.append(test.__name__)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ {test.__name__}: Unexpected error: {e}")
|
||||||
|
failed.append(test.__name__)
|
||||||
|
|
||||||
|
print(f"\n{'='*50}")
|
||||||
|
if failed:
|
||||||
|
print(f"FAILED: {len(failed)} test(s) failed")
|
||||||
|
for name in failed:
|
||||||
|
print(f" - {name}")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print(f"SUCCESS: All {len(tests)} tests passed!")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run_tests()
|
||||||
Reference in New Issue
Block a user