#!/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()