diff --git a/dashboard/habits.html b/dashboard/habits.html index d930605..4b5289e 100644 --- a/dashboard/habits.html +++ b/dashboard/habits.html @@ -516,6 +516,7 @@ // Create habit async function createHabit() { const nameInput = document.getElementById('habitName'); + const createBtn = document.getElementById('habitCreateBtn'); const name = nameInput.value.trim(); const frequency = document.querySelector('input[name="frequency"]:checked').value; @@ -524,6 +525,11 @@ return; } + // Disable button during submission (loading state) + createBtn.disabled = true; + const originalText = createBtn.textContent; + createBtn.textContent = 'Se creează...'; + try { const response = await fetch('/api/habits', { method: 'POST', @@ -532,16 +538,27 @@ }); if (response.ok) { + // Clear input field after successful creation + nameInput.value = ''; + hideHabitModal(); showToast('Obișnuință creată cu succes!'); loadHabits(); } else { const error = await response.text(); showToast('Eroare la crearea obișnuinței: ' + error); + + // Re-enable button on error (modal stays open) + createBtn.disabled = false; + createBtn.textContent = originalText; } } catch (error) { showToast('Eroare la conectarea cu serverul'); console.error(error); + + // Re-enable button on error (modal stays open) + createBtn.disabled = false; + createBtn.textContent = originalText; } } diff --git a/dashboard/habits.json b/dashboard/habits.json index 0a4b66b..b1b63ca 100644 --- a/dashboard/habits.json +++ b/dashboard/habits.json @@ -1,4 +1,4 @@ { - "lastUpdated": "2026-02-10T11:59:02.042Z", - "habits": [] -} \ No newline at end of file + "habits": [], + "lastUpdated": "2026-02-10T12:39:00Z" +} diff --git a/dashboard/test_habits_form_submit.py b/dashboard/test_habits_form_submit.py new file mode 100644 index 0000000..2ada102 --- /dev/null +++ b/dashboard/test_habits_form_submit.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +""" +Tests for Story 11.0: Frontend - Create habit from form + +Acceptance Criteria: + 1. Form submit calls POST /api/habits with name and frequency + 2. Shows loading state on submit (button disabled) + 3. On success: modal closes, list refreshes, new habit appears + 4. On error: shows error message in modal, modal stays open + 5. Input field cleared after successful creation + 6. Tests for form submission pass +""" + +import os +import sys + +def test_form_submit_api_call(): + """Test that createHabit calls POST /api/habits with correct data""" + with open('dashboard/habits.html', 'r') as f: + html = f.read() + + # Check that createHabit function exists + assert 'async function createHabit()' in html, "createHabit function should exist" + + # Check that it calls POST /api/habits + assert "fetch('/api/habits'" in html, "Should fetch /api/habits endpoint" + assert "method: 'POST'" in html, "Should use POST method" + + # Check that it sends name and frequency in JSON body + assert "body: JSON.stringify({ name, frequency })" in html, "Should send name and frequency in request body" + + # Check that frequency is read from checked radio button + assert "document.querySelector('input[name=\"frequency\"]:checked').value" in html, "Should read frequency from radio buttons" + + print("✓ Form submission calls POST /api/habits with name and frequency") + +def test_loading_state_on_submit(): + """Test that button is disabled during submission""" + with open('dashboard/habits.html', 'r') as f: + html = f.read() + + # Check that createBtn is referenced + assert "createBtn = document.getElementById('habitCreateBtn')" in html, "Should get create button element" + + # Check that button is disabled before fetch + assert "createBtn.disabled = true" in html, "Button should be disabled during submission" + + # Check that button text changes to loading state + assert "createBtn.textContent = 'Se creează...'" in html or "Se creează" in html, "Should show loading text during submission" + + # Check that original text is stored for restoration + assert "originalText = createBtn.textContent" in html, "Should store original button text" + + print("✓ Shows loading state on submit (button disabled)") + +def test_success_behavior(): + """Test behavior on successful habit creation""" + with open('dashboard/habits.html', 'r') as f: + html = f.read() + + # Check for success handling block + assert "if (response.ok)" in html, "Should check for successful response" + + # Check that modal closes on success + assert "hideHabitModal()" in html, "Modal should close on success" + + # Check that habits list is refreshed + assert "loadHabits()" in html, "Should reload habits list on success" + + # Check that success toast is shown + assert "showToast(" in html and "succes" in html, "Should show success toast" + + print("✓ On success: modal closes, list refreshes, new habit appears") + +def test_error_behavior(): + """Test behavior on error (modal stays open, shows error)""" + with open('dashboard/habits.html', 'r') as f: + html = f.read() + + # Check for error handling in response + assert "else {" in html or "!response.ok" in html, "Should handle error responses" + + # Check for catch block for network errors + assert "catch (error)" in html, "Should catch network errors" + + # Check that error toast is shown + assert "showToast('Eroare" in html, "Should show error toast on failure" + + # Check that button is re-enabled on error (so modal stays usable) + createHabit_block = html[html.find('async function createHabit()'):html.find('async function createHabit()') + 2000] + + # Count occurrences of button re-enable in error paths + error_section = html[html.find('} else {'):html.find('} catch (error)') + 500] + assert 'createBtn.disabled = false' in error_section, "Button should be re-enabled on error" + assert 'createBtn.textContent = originalText' in error_section, "Button text should be restored on error" + + print("✓ On error: shows error message, modal stays open (button re-enabled)") + +def test_input_cleared_after_success(): + """Test that input field is cleared after successful creation""" + with open('dashboard/habits.html', 'r') as f: + html = f.read() + + # Find the success block + success_section = html[html.find('if (response.ok)'):html.find('if (response.ok)') + 500] + + # Check that nameInput.value is cleared + assert "nameInput.value = ''" in success_section or 'nameInput.value = ""' in success_section, \ + "Input field should be cleared after successful creation" + + print("✓ Input field cleared after successful creation") + +def test_form_validation_still_works(): + """Test that existing form validation is still in place""" + with open('dashboard/habits.html', 'r') as f: + html = f.read() + + # Check that empty name validation exists + assert "if (!name)" in html, "Should validate for empty name" + assert "name = nameInput.value.trim()" in html, "Should trim name before validation" + + # Check that create button is disabled when name is empty + assert "nameInput.addEventListener('input'" in html, "Should listen to input changes" + assert "createBtn.disabled = name.length === 0" in html, "Button should be disabled when name is empty" + + print("✓ Form validation still works (empty name check)") + +def test_modal_reset_on_open(): + """Test that modal resets form when opened""" + with open('dashboard/habits.html', 'r') as f: + html = f.read() + + # Check showAddHabitModal function + assert 'function showAddHabitModal()' in html, "showAddHabitModal function should exist" + + # Check that form is reset when modal opens + modal_function = html[html.find('function showAddHabitModal()'):html.find('function showAddHabitModal()') + 500] + assert "nameInput.value = ''" in modal_function or 'nameInput.value = ""' in modal_function, \ + "Should clear name input when opening modal" + + print("✓ Modal resets form when opened") + +def test_enter_key_submission(): + """Test that Enter key can submit the form""" + with open('dashboard/habits.html', 'r') as f: + html = f.read() + + # Check for Enter key handler + assert "addEventListener('keypress'" in html, "Should listen for keypress events" + assert "e.key === 'Enter'" in html, "Should check for Enter key" + assert "!createBtn.disabled" in html, "Should check if button is enabled before submitting" + assert "createHabit()" in html, "Should call createHabit on Enter" + + print("✓ Enter key submits form when button is enabled") + +def test_all_acceptance_criteria(): + """Summary test - verify all acceptance criteria are met""" + with open('dashboard/habits.html', 'r') as f: + html = f.read() + + checks = [ + ("'/api/habits'" in html and "method: 'POST'" in html and "body: JSON.stringify({ name, frequency })" in html, + "1. Form submit calls POST /api/habits with name and frequency"), + + ("createBtn.disabled = true" in html and "Se creează" in html, + "2. Shows loading state on submit (button disabled)"), + + ("hideHabitModal()" in html and "loadHabits()" in html and "response.ok" in html, + "3. On success: modal closes, list refreshes, new habit appears"), + + ("catch (error)" in html and "createBtn.disabled = false" in html, + "4. On error: shows error message, modal stays open"), + + ("nameInput.value = ''" in html or 'nameInput.value = ""' in html, + "5. Input field cleared after successful creation"), + + (True, "6. Tests for form submission pass (this test!)") + ] + + all_pass = True + for condition, description in checks: + status = "✓" if condition else "✗" + print(f" {status} {description}") + if not condition: + all_pass = False + + assert all_pass, "Not all acceptance criteria are met" + print("\n✓ All acceptance criteria verified!") + +if __name__ == '__main__': + try: + test_form_submit_api_call() + test_loading_state_on_submit() + test_success_behavior() + test_error_behavior() + test_input_cleared_after_success() + test_form_validation_still_works() + test_modal_reset_on_open() + test_enter_key_submission() + test_all_acceptance_criteria() + + print("\n✅ All Story 11.0 tests passed!") + sys.exit(0) + except AssertionError as e: + print(f"\n❌ Test failed: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"\n❌ Unexpected error: {e}", file=sys.stderr) + sys.exit(1)