343 lines
13 KiB
Python
343 lines
13 KiB
Python
#!/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()
|