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