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)