diff --git a/05_react/unterricht/tag45/01_react-app-with-tests/src/components/forms/FormContact.jsx b/05_react/unterricht/tag45/01_react-app-with-tests/src/components/forms/FormContact.jsx
index 5c7fd3d..c805c11 100644
--- a/05_react/unterricht/tag45/01_react-app-with-tests/src/components/forms/FormContact.jsx
+++ b/05_react/unterricht/tag45/01_react-app-with-tests/src/components/forms/FormContact.jsx
@@ -12,6 +12,7 @@ const FormContact = (props) => {
});
const [errors, setErrors] = useState({});
+ const [successMessage, setSuccessMessage] = useState('');
// EventHandlers =======
const handleSubmit = (e) => {
@@ -22,8 +23,10 @@ const FormContact = (props) => {
const errors = validate(formData);
if (Object.keys(errors).length === 0) {
- console.log('Form submitted ....');
+ console.log('Form Data', formData);
// TODO: send over AJAX/Fetch
+ setSuccessMessage('Form submitted');
+
form.reset();
setFormData({
firstname: '', //
@@ -35,6 +38,7 @@ const FormContact = (props) => {
} else {
console.warn('Form submit error');
setErrors(errors);
+ setSuccessMessage('');
}
};
@@ -151,6 +155,12 @@ const FormContact = (props) => {
+
+ {successMessage && (
+
+ {successMessage}
+
+ )}
);
};
diff --git a/05_react/unterricht/tag45/01_react-app-with-tests/src/test/FormContact.test.jsx b/05_react/unterricht/tag45/01_react-app-with-tests/src/test/FormContact.test.jsx
new file mode 100644
index 0000000..37dbc9c
--- /dev/null
+++ b/05_react/unterricht/tag45/01_react-app-with-tests/src/test/FormContact.test.jsx
@@ -0,0 +1,402 @@
+import { render, screen } from '@testing-library/react';
+import FormContact from '../components/forms/FormContact';
+import userEvent from '@testing-library/user-event';
+
+import { expect, vi, afterAll, describe, test } from 'vitest';
+
+const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); //
+
+afterAll(() => {
+ consoleSpy.mockRestore();
+});
+
+// Test Suite
+describe('FormContact', () => {
+ // Test Case
+ test('FormContact submits data object correctly', async () => {
+ render();
+
+ const user = userEvent.setup();
+
+ // find the input
+ const firstNameInput = screen.getByLabelText('Firstname');
+ const lastNameInput = screen.getByLabelText('Lastname');
+ const emailInput = screen.getByLabelText('E-Mail');
+ const messageInput = screen.getByLabelText('Message');
+ const submitButton = screen.getByRole('button', { name: 'Send Message' });
+
+ await user.type(firstNameInput, 'John');
+ await user.type(lastNameInput, 'Wick');
+ await user.type(emailInput, 'john.wick@continentalhotel.com');
+ await user.type(messageInput, 'Hello, this is a test message');
+
+ await user.click(submitButton);
+
+ // const succesMessageDiv = screen.getByTestId('success-message'); // data-testid Attribut muss gesetzt werden
+ // const succesMessageDiv = screen.getByRole('note', { name: /Form submitted/i }); // data-testid Attribut muss gesetzt werden
+ // const succesMessageDiv = screen.getByText(/Form submitted/i); // data-testid Attribut muss gesetzt werden
+ // expect(succesMessageDiv).toHaveTextContent(/Form submitted/i);
+
+ expect(consoleSpy).toHaveBeenCalledWith('Form Data', {
+ firstname: 'John',
+ lastname: 'Wick',
+ email: 'john.wick@continentalhotel.com',
+ message: 'Hello, this is a test message',
+ });
+ });
+
+ // ===== FIRSTNAME FIELD TESTS =====
+ describe('Firstname field error messages', () => {
+ test('shows error message when firstname is empty on submit', async () => {
+ render();
+ const user = userEvent.setup();
+
+ const lastNameInput = screen.getByLabelText('Lastname');
+ const emailInput = screen.getByLabelText('E-Mail');
+ const messageInput = screen.getByLabelText('Message');
+ const submitButton = screen.getByRole('button', { name: 'Send Message' });
+
+ await user.type(lastNameInput, 'Wick');
+ await user.type(emailInput, 'john@example.com');
+ await user.type(messageInput, 'This is a valid test message');
+
+ await user.click(submitButton);
+
+ expect(screen.getByText('Vorname ist erforderlich.')).toBeInTheDocument();
+ });
+
+ test('shows error message when firstname is too short (less than 2 chars)', async () => {
+ render();
+ const user = userEvent.setup();
+
+ const firstNameInput = screen.getByLabelText('Firstname');
+ const submitButton = screen.getByRole('button', { name: 'Send Message' });
+
+ await user.type(firstNameInput, 'J');
+ await user.click(submitButton);
+
+ expect(
+ screen.getByText('Vorname muss mindestens 2 Zeichen und maximal 60 Zeichen lang sein.'),
+ ).toBeInTheDocument();
+ });
+
+ test('shows error message when firstname is too long (more than 60 chars)', async () => {
+ render();
+ const user = userEvent.setup();
+
+ const firstNameInput = screen.getByLabelText('Firstname');
+ const submitButton = screen.getByRole('button', { name: 'Send Message' });
+
+ const longName = 'a'.repeat(61);
+ await user.type(firstNameInput, longName);
+ await user.click(submitButton);
+
+ expect(
+ screen.getByText('Vorname muss mindestens 2 Zeichen und maximal 60 Zeichen lang sein.'),
+ ).toBeInTheDocument();
+ });
+
+ test('no error message when firstname is valid', async () => {
+ render();
+ const user = userEvent.setup();
+
+ const firstNameInput = screen.getByLabelText('Firstname');
+ const lastNameInput = screen.getByLabelText('Lastname');
+ const emailInput = screen.getByLabelText('E-Mail');
+ const messageInput = screen.getByLabelText('Message');
+
+ await user.type(firstNameInput, 'John');
+ await user.type(lastNameInput, 'Wick');
+ await user.type(emailInput, 'john@example.com');
+ await user.type(messageInput, 'This is a valid test message');
+
+ // Trigger onBlur to validate
+ await user.tab();
+
+ expect(firstNameInput).not.toHaveClass('is-invalid');
+ });
+ });
+
+ // ===== LASTNAME FIELD TESTS =====
+ describe('Lastname field error messages', () => {
+ test('shows error message when lastname is empty on submit', async () => {
+ render();
+ const user = userEvent.setup();
+
+ const firstNameInput = screen.getByLabelText('Firstname');
+ const emailInput = screen.getByLabelText('E-Mail');
+ const messageInput = screen.getByLabelText('Message');
+ const submitButton = screen.getByRole('button', { name: 'Send Message' });
+
+ await user.type(firstNameInput, 'John');
+ await user.type(emailInput, 'john@example.com');
+ await user.type(messageInput, 'This is a valid test message');
+
+ await user.click(submitButton);
+
+ expect(screen.getByText('Nachname ist erforderlich.')).toBeInTheDocument();
+ });
+
+ test('shows error message when lastname is too short (less than 2 chars)', async () => {
+ render();
+ const user = userEvent.setup();
+
+ const lastNameInput = screen.getByLabelText('Lastname');
+ const submitButton = screen.getByRole('button', { name: 'Send Message' });
+
+ await user.type(lastNameInput, 'W');
+ await user.click(submitButton);
+
+ expect(
+ screen.getByText('Nachname muss mindestens 2 Zeichen und maximal 60 Zeichen lang sein.'),
+ ).toBeInTheDocument();
+ });
+
+ test('shows error message when lastname is too long (more than 60 chars)', async () => {
+ render();
+ const user = userEvent.setup();
+
+ const lastNameInput = screen.getByLabelText('Lastname');
+ const submitButton = screen.getByRole('button', { name: 'Send Message' });
+
+ const longName = 'b'.repeat(61);
+ await user.type(lastNameInput, longName);
+ await user.click(submitButton);
+
+ expect(
+ screen.getByText('Nachname muss mindestens 2 Zeichen und maximal 60 Zeichen lang sein.'),
+ ).toBeInTheDocument();
+ });
+
+ test('no error message when lastname is valid', async () => {
+ render();
+ const user = userEvent.setup();
+
+ const firstNameInput = screen.getByLabelText('Firstname');
+ const lastNameInput = screen.getByLabelText('Lastname');
+ const emailInput = screen.getByLabelText('E-Mail');
+ const messageInput = screen.getByLabelText('Message');
+
+ await user.type(firstNameInput, 'John');
+ await user.type(lastNameInput, 'Wick');
+ await user.type(emailInput, 'john@example.com');
+ await user.type(messageInput, 'This is a valid test message');
+
+ // Trigger onBlur to validate
+ await user.tab();
+
+ expect(lastNameInput).not.toHaveClass('is-invalid');
+ });
+ });
+
+ // ===== EMAIL FIELD TESTS =====
+ describe('Email field error messages', () => {
+ test('shows error message when email is empty on submit', async () => {
+ render();
+ const user = userEvent.setup();
+
+ const firstNameInput = screen.getByLabelText('Firstname');
+ const lastNameInput = screen.getByLabelText('Lastname');
+ const messageInput = screen.getByLabelText('Message');
+ const submitButton = screen.getByRole('button', { name: 'Send Message' });
+
+ await user.type(firstNameInput, 'John');
+ await user.type(lastNameInput, 'Wick');
+ await user.type(messageInput, 'This is a valid test message');
+
+ await user.click(submitButton);
+
+ expect(screen.getByText('E-Mail ist erforderlich.')).toBeInTheDocument();
+ });
+
+ test('shows error message when email format is invalid', async () => {
+ render();
+ const user = userEvent.setup();
+
+ const emailInput = screen.getByLabelText('E-Mail');
+ const submitButton = screen.getByRole('button', { name: 'Send Message' });
+
+ await user.type(emailInput, 'invalidemail');
+ await user.click(submitButton);
+
+ expect(screen.getByText('Bitte gib eine gültige E-Mail-Adresse ein.')).toBeInTheDocument();
+ });
+
+ test('shows error message when email is missing @ symbol', async () => {
+ render();
+ const user = userEvent.setup();
+
+ const emailInput = screen.getByLabelText('E-Mail');
+ const submitButton = screen.getByRole('button', { name: 'Send Message' });
+
+ await user.type(emailInput, 'johnexample.com');
+ await user.click(submitButton);
+
+ expect(screen.getByText('Bitte gib eine gültige E-Mail-Adresse ein.')).toBeInTheDocument();
+ });
+
+ test('shows error message when email is missing domain', async () => {
+ render();
+ const user = userEvent.setup();
+
+ const emailInput = screen.getByLabelText('E-Mail');
+ const submitButton = screen.getByRole('button', { name: 'Send Message' });
+
+ await user.type(emailInput, 'john@');
+ await user.click(submitButton);
+
+ expect(screen.getByText('Bitte gib eine gültige E-Mail-Adresse ein.')).toBeInTheDocument();
+ });
+
+ test('no error message when email is valid', async () => {
+ render();
+ const user = userEvent.setup();
+
+ const firstNameInput = screen.getByLabelText('Firstname');
+ const lastNameInput = screen.getByLabelText('Lastname');
+ const emailInput = screen.getByLabelText('E-Mail');
+ const messageInput = screen.getByLabelText('Message');
+
+ await user.type(firstNameInput, 'John');
+ await user.type(lastNameInput, 'Wick');
+ await user.type(emailInput, 'john@example.com');
+ await user.type(messageInput, 'This is a valid test message');
+
+ // Trigger onBlur to validate
+ await user.tab();
+
+ expect(emailInput).not.toHaveClass('is-invalid');
+ });
+ });
+
+ // ===== MESSAGE FIELD TESTS =====
+ describe('Message field error messages', () => {
+ test('shows error message when message is empty on submit', async () => {
+ render();
+ const user = userEvent.setup();
+
+ const firstNameInput = screen.getByLabelText('Firstname');
+ const lastNameInput = screen.getByLabelText('Lastname');
+ const emailInput = screen.getByLabelText('E-Mail');
+ const submitButton = screen.getByRole('button', { name: 'Send Message' });
+
+ await user.type(firstNameInput, 'John');
+ await user.type(lastNameInput, 'Wick');
+ await user.type(emailInput, 'john@example.com');
+
+ await user.click(submitButton);
+
+ expect(screen.getByText('Nachricht ist erforderlich.')).toBeInTheDocument();
+ });
+
+ test('shows error message when message is too short (less than 10 chars)', async () => {
+ render();
+ const user = userEvent.setup();
+
+ const messageInput = screen.getByLabelText('Message');
+ const submitButton = screen.getByRole('button', { name: 'Send Message' });
+
+ await user.type(messageInput, 'short msg');
+ await user.click(submitButton);
+
+ expect(
+ screen.getByText('Die Nachricht muss mindestens 10 und maximal 1000 Zeichen lang sein.'),
+ ).toBeInTheDocument();
+ });
+
+ test('shows error message when message is too long (more than 1000 chars)', async () => {
+ render();
+ const user = userEvent.setup();
+
+ const messageInput = screen.getByLabelText('Message');
+ const submitButton = screen.getByRole('button', { name: 'Send Message' });
+
+ const longMessage = 'a'.repeat(1001);
+ await user.type(messageInput, longMessage);
+ await user.click(submitButton);
+
+ expect(
+ screen.getByText('Die Nachricht muss mindestens 10 und maximal 1000 Zeichen lang sein.'),
+ ).toBeInTheDocument();
+ });
+
+ test('no error message when message is valid', async () => {
+ render();
+ const user = userEvent.setup();
+
+ const firstNameInput = screen.getByLabelText('Firstname');
+ const lastNameInput = screen.getByLabelText('Lastname');
+ const emailInput = screen.getByLabelText('E-Mail');
+ const messageInput = screen.getByLabelText('Message');
+
+ await user.type(firstNameInput, 'John');
+ await user.type(lastNameInput, 'Wick');
+ await user.type(emailInput, 'john@example.com');
+ await user.type(messageInput, 'This is a valid test message');
+
+ // Trigger onBlur to validate
+ await user.tab();
+
+ expect(messageInput).not.toHaveClass('is-invalid');
+ });
+ });
+
+ // ===== ONBLUR VALIDATION TESTS =====
+ describe('Form field onBlur validation', () => {
+ test('shows error on firstname field after blur when invalid', async () => {
+ render();
+ const user = userEvent.setup();
+
+ const firstNameInput = screen.getByLabelText('Firstname');
+
+ await user.type(firstNameInput, 'J');
+ await user.click(screen.getByLabelText('Lastname')); // Trigger blur on firstname
+
+ expect(firstNameInput).toHaveClass('is-invalid');
+ });
+
+ test('clears error on firstname field after blur when valid', async () => {
+ render();
+ const user = userEvent.setup();
+
+ const firstNameInput = screen.getByLabelText('Firstname');
+ const lastNameInput = screen.getByLabelText('Lastname');
+
+ await user.type(firstNameInput, 'J');
+ await user.click(lastNameInput); // Trigger blur with invalid input
+ expect(firstNameInput).toHaveClass('is-invalid');
+
+ // Clear and type valid input
+ await user.clear(firstNameInput);
+ await user.type(firstNameInput, 'John');
+ await user.click(screen.getByLabelText('E-Mail')); // Trigger blur with valid input
+
+ expect(firstNameInput).not.toHaveClass('is-invalid');
+ });
+
+ test('shows error on email field after blur when invalid', async () => {
+ render();
+ const user = userEvent.setup();
+
+ const emailInput = screen.getByLabelText('E-Mail');
+
+ await user.type(emailInput, 'invalidemail');
+ await user.click(screen.getByLabelText('Message')); // Trigger blur on email
+
+ expect(emailInput).toHaveClass('is-invalid');
+ });
+
+ test('shows error on message field after blur when too short', async () => {
+ render();
+ const user = userEvent.setup();
+
+ const messageInput = screen.getByLabelText('Message');
+ const submitButton = screen.getByRole('button', { name: 'Send Message' });
+
+ await user.type(messageInput, 'short');
+ await user.click(submitButton); // Trigger blur on message
+
+ expect(messageInput).toHaveClass('is-invalid');
+ });
+ });
+});
diff --git a/05_react/unterricht/tag45/01_react-app-with-tests/src/utils/validators/validate.js b/05_react/unterricht/tag45/01_react-app-with-tests/src/utils/validators/validate.js
index dcbdcb7..a186292 100644
--- a/05_react/unterricht/tag45/01_react-app-with-tests/src/utils/validators/validate.js
+++ b/05_react/unterricht/tag45/01_react-app-with-tests/src/utils/validators/validate.js
@@ -7,7 +7,7 @@ const validate = (formData) => {
if (validator.isEmpty(formData.firstname.trim())) {
errors.firstname = 'Vorname ist erforderlich.';
} else if (!validator.isLength(formData.firstname.trim(), { min: 2, max: 60 })) {
- errors.firstname = 'Vorname muss mindestens 2 Zeichen und maximal 60 Zeichen lang sein.';
+ errors.firstname = 'Vorname muss mindestens 2 Zeichen und maximal 60 Zeichen lang sein.';
}
// Lastname
diff --git a/05_react/unterricht/tag45/meeting.md b/05_react/unterricht/tag45/meeting.md
new file mode 100644
index 0000000..7ca6517
--- /dev/null
+++ b/05_react/unterricht/tag45/meeting.md
@@ -0,0 +1,6 @@
+# Meeting Fr. 14.08.2026
+
+- [ ] 14:10 - 14:20 Uhr Ersin
+- [ ] 14:20 - 14:30 Uhr Kahleel
+- [ ] 14:30 - 14:40 Uhr Adel
+- [ ] 14:40 - 14:50 Uhr Andreas