/** * Component tests for authentication forms * Tests LoginForm and RegisterForm Svelte components */ import { render, fireEvent, screen, waitFor } from '@testing-library/svelte'; import { describe, it, expect, vi } from 'vitest'; import LoginForm from '$lib/components/auth/LoginForm.svelte'; import RegisterForm from '$lib/components/auth/RegisterForm.svelte'; describe('LoginForm', () => { describe('Rendering', () => { it('renders email and password fields', () => { render(LoginForm); expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); expect(screen.getByLabelText(/password/i)).toBeInTheDocument(); }); it('renders submit button with correct text', () => { render(LoginForm); const button = screen.getByRole('button', { name: /login/i }); expect(button).toBeInTheDocument(); expect(button).not.toBeDisabled(); }); it('shows loading state when isLoading prop is true', () => { render(LoginForm, { props: { isLoading: true } }); const button = screen.getByRole('button'); expect(button).toBeDisabled(); expect(screen.getByText(/logging in/i)).toBeInTheDocument(); }); it('has proper autocomplete attributes', () => { render(LoginForm); const emailInput = screen.getByLabelText(/email/i); const passwordInput = screen.getByLabelText(/password/i); expect(emailInput).toHaveAttribute('autocomplete', 'email'); expect(passwordInput).toHaveAttribute('autocomplete', 'current-password'); }); }); describe('Validation', () => { it('shows error when email is empty on submit', async () => { render(LoginForm); const button = screen.getByRole('button', { name: /login/i }); await fireEvent.click(button); expect(await screen.findByText(/email is required/i)).toBeInTheDocument(); }); it('shows error when email is invalid', async () => { render(LoginForm); const emailInput = screen.getByLabelText(/email/i); await fireEvent.input(emailInput, { target: { value: 'invalid-email' } }); const button = screen.getByRole('button', { name: /login/i }); await fireEvent.click(button); expect(await screen.findByText(/valid email address/i)).toBeInTheDocument(); }); it('shows error when password is empty on submit', async () => { render(LoginForm); const emailInput = screen.getByLabelText(/email/i); await fireEvent.input(emailInput, { target: { value: 'test@example.com' } }); const button = screen.getByRole('button', { name: /login/i }); await fireEvent.click(button); expect(await screen.findByText(/password is required/i)).toBeInTheDocument(); }); it('accepts valid email formats', async () => { const validEmails = ['test@example.com', 'user+tag@domain.co.uk', 'first.last@example.com']; for (const email of validEmails) { const { unmount } = render(LoginForm); const emailInput = screen.getByLabelText(/email/i); await fireEvent.input(emailInput, { target: { value: email } }); const passwordInput = screen.getByLabelText(/password/i); await fireEvent.input(passwordInput, { target: { value: 'password123' } }); const button = screen.getByRole('button', { name: /login/i }); await fireEvent.click(button); // Should not show email error expect(screen.queryByText(/valid email address/i)).not.toBeInTheDocument(); unmount(); } }); it('clears errors when form is corrected', async () => { render(LoginForm); // Submit with empty email const button = screen.getByRole('button', { name: /login/i }); await fireEvent.click(button); expect(await screen.findByText(/email is required/i)).toBeInTheDocument(); // Fix email const emailInput = screen.getByLabelText(/email/i); await fireEvent.input(emailInput, { target: { value: 'test@example.com' } }); // Submit again await fireEvent.click(button); // Email error should be gone, but password error should appear expect(screen.queryByText(/email is required/i)).not.toBeInTheDocument(); expect(await screen.findByText(/password is required/i)).toBeInTheDocument(); }); }); describe('Submission', () => { it('dispatches submit event with correct data on valid form', async () => { const { component } = render(LoginForm); const submitHandler = vi.fn(); component.$on('submit', submitHandler); const emailInput = screen.getByLabelText(/email/i); await fireEvent.input(emailInput, { target: { value: 'test@example.com' } }); const passwordInput = screen.getByLabelText(/password/i); await fireEvent.input(passwordInput, { target: { value: 'TestPassword123' } }); const button = screen.getByRole('button', { name: /login/i }); await fireEvent.click(button); await waitFor(() => { expect(submitHandler).toHaveBeenCalledTimes(1); }); const event = submitHandler.mock.calls[0][0]; expect(event.detail).toEqual({ email: 'test@example.com', password: 'TestPassword123', }); }); it('does not dispatch submit event when form is invalid', async () => { const { component } = render(LoginForm); const submitHandler = vi.fn(); component.$on('submit', submitHandler); // Try to submit with empty fields const button = screen.getByRole('button', { name: /login/i }); await fireEvent.click(button); await waitFor(() => { expect(screen.getByText(/email is required/i)).toBeInTheDocument(); }); expect(submitHandler).not.toHaveBeenCalled(); }); it('disables all inputs when loading', () => { render(LoginForm, { props: { isLoading: true } }); const emailInput = screen.getByLabelText(/email/i); const passwordInput = screen.getByLabelText(/password/i); const button = screen.getByRole('button'); expect(emailInput).toBeDisabled(); expect(passwordInput).toBeDisabled(); expect(button).toBeDisabled(); }); }); }); describe('RegisterForm', () => { describe('Rendering', () => { it('renders all required fields', () => { render(RegisterForm); expect(screen.getByLabelText(/^email$/i)).toBeInTheDocument(); expect(screen.getByLabelText(/^password$/i)).toBeInTheDocument(); expect(screen.getByLabelText(/confirm password/i)).toBeInTheDocument(); }); it('renders submit button with correct text', () => { render(RegisterForm); const button = screen.getByRole('button', { name: /create account/i }); expect(button).toBeInTheDocument(); expect(button).not.toBeDisabled(); }); it('shows password requirements help text', () => { render(RegisterForm); expect( screen.getByText(/must be 8\+ characters with uppercase, lowercase, and number/i) ).toBeInTheDocument(); }); it('shows loading state when isLoading prop is true', () => { render(RegisterForm, { props: { isLoading: true } }); const button = screen.getByRole('button'); expect(button).toBeDisabled(); expect(screen.getByText(/creating account/i)).toBeInTheDocument(); }); it('has proper autocomplete attributes', () => { render(RegisterForm); const emailInput = screen.getByLabelText(/^email$/i); const passwordInput = screen.getByLabelText(/^password$/i); const confirmPasswordInput = screen.getByLabelText(/confirm password/i); expect(emailInput).toHaveAttribute('autocomplete', 'email'); expect(passwordInput).toHaveAttribute('autocomplete', 'new-password'); expect(confirmPasswordInput).toHaveAttribute('autocomplete', 'new-password'); }); }); describe('Email Validation', () => { it('shows error when email is empty', async () => { render(RegisterForm); const button = screen.getByRole('button', { name: /create account/i }); await fireEvent.click(button); expect(await screen.findByText(/email is required/i)).toBeInTheDocument(); }); it('shows error when email is invalid', async () => { render(RegisterForm); const emailInput = screen.getByLabelText(/^email$/i); await fireEvent.input(emailInput, { target: { value: 'not-an-email' } }); const button = screen.getByRole('button', { name: /create account/i }); await fireEvent.click(button); expect(await screen.findByText(/valid email address/i)).toBeInTheDocument(); }); }); describe('Password Strength Validation', () => { it('shows error when password is too short', async () => { render(RegisterForm); const passwordInput = screen.getByLabelText(/^password$/i); await fireEvent.input(passwordInput, { target: { value: 'Test1' } }); const button = screen.getByRole('button', { name: /create account/i }); await fireEvent.click(button); expect(await screen.findByText(/at least 8 characters/i)).toBeInTheDocument(); }); it('shows error when password lacks uppercase letter', async () => { render(RegisterForm); const passwordInput = screen.getByLabelText(/^password$/i); await fireEvent.input(passwordInput, { target: { value: 'testpassword123' } }); const button = screen.getByRole('button', { name: /create account/i }); await fireEvent.click(button); expect(await screen.findByText(/uppercase letter/i)).toBeInTheDocument(); }); it('shows error when password lacks lowercase letter', async () => { render(RegisterForm); const passwordInput = screen.getByLabelText(/^password$/i); await fireEvent.input(passwordInput, { target: { value: 'TESTPASSWORD123' } }); const button = screen.getByRole('button', { name: /create account/i }); await fireEvent.click(button); expect(await screen.findByText(/lowercase letter/i)).toBeInTheDocument(); }); it('shows error when password lacks number', async () => { render(RegisterForm); const passwordInput = screen.getByLabelText(/^password$/i); await fireEvent.input(passwordInput, { target: { value: 'TestPassword' } }); const button = screen.getByRole('button', { name: /create account/i }); await fireEvent.click(button); expect(await screen.findByText(/contain a number/i)).toBeInTheDocument(); }); it('accepts valid password meeting all requirements', async () => { render(RegisterForm); const emailInput = screen.getByLabelText(/^email$/i); await fireEvent.input(emailInput, { target: { value: 'test@example.com' } }); const passwordInput = screen.getByLabelText(/^password$/i); await fireEvent.input(passwordInput, { target: { value: 'ValidPassword123' } }); const confirmPasswordInput = screen.getByLabelText(/confirm password/i); await fireEvent.input(confirmPasswordInput, { target: { value: 'ValidPassword123' } }); const button = screen.getByRole('button', { name: /create account/i }); await fireEvent.click(button); // Should not show password strength errors expect(screen.queryByText(/at least 8 characters/i)).not.toBeInTheDocument(); expect(screen.queryByText(/uppercase letter/i)).not.toBeInTheDocument(); expect(screen.queryByText(/lowercase letter/i)).not.toBeInTheDocument(); expect(screen.queryByText(/contain a number/i)).not.toBeInTheDocument(); }); }); describe('Password Confirmation Validation', () => { it('shows error when confirm password is empty', async () => { render(RegisterForm); const emailInput = screen.getByLabelText(/^email$/i); await fireEvent.input(emailInput, { target: { value: 'test@example.com' } }); const passwordInput = screen.getByLabelText(/^password$/i); await fireEvent.input(passwordInput, { target: { value: 'ValidPassword123' } }); const button = screen.getByRole('button', { name: /create account/i }); await fireEvent.click(button); expect(await screen.findByText(/confirm your password/i)).toBeInTheDocument(); }); it('shows error when passwords do not match', async () => { render(RegisterForm); const emailInput = screen.getByLabelText(/^email$/i); await fireEvent.input(emailInput, { target: { value: 'test@example.com' } }); const passwordInput = screen.getByLabelText(/^password$/i); await fireEvent.input(passwordInput, { target: { value: 'ValidPassword123' } }); const confirmPasswordInput = screen.getByLabelText(/confirm password/i); await fireEvent.input(confirmPasswordInput, { target: { value: 'DifferentPassword123' } }); const button = screen.getByRole('button', { name: /create account/i }); await fireEvent.click(button); expect(await screen.findByText(/passwords do not match/i)).toBeInTheDocument(); }); it('accepts matching passwords', async () => { render(RegisterForm); const emailInput = screen.getByLabelText(/^email$/i); await fireEvent.input(emailInput, { target: { value: 'test@example.com' } }); const passwordInput = screen.getByLabelText(/^password$/i); await fireEvent.input(passwordInput, { target: { value: 'ValidPassword123' } }); const confirmPasswordInput = screen.getByLabelText(/confirm password/i); await fireEvent.input(confirmPasswordInput, { target: { value: 'ValidPassword123' } }); const button = screen.getByRole('button', { name: /create account/i }); await fireEvent.click(button); // Should not show confirmation error expect(screen.queryByText(/passwords do not match/i)).not.toBeInTheDocument(); }); }); describe('Submission', () => { it('dispatches submit event with correct data on valid form', async () => { const { component } = render(RegisterForm); const submitHandler = vi.fn(); component.$on('submit', submitHandler); const emailInput = screen.getByLabelText(/^email$/i); await fireEvent.input(emailInput, { target: { value: 'test@example.com' } }); const passwordInput = screen.getByLabelText(/^password$/i); await fireEvent.input(passwordInput, { target: { value: 'ValidPassword123' } }); const confirmPasswordInput = screen.getByLabelText(/confirm password/i); await fireEvent.input(confirmPasswordInput, { target: { value: 'ValidPassword123' } }); const button = screen.getByRole('button', { name: /create account/i }); await fireEvent.click(button); await waitFor(() => { expect(submitHandler).toHaveBeenCalledTimes(1); }); const event = submitHandler.mock.calls[0][0]; expect(event.detail).toEqual({ email: 'test@example.com', password: 'ValidPassword123', }); }); it('does not include confirmPassword in submit event', async () => { const { component } = render(RegisterForm); const submitHandler = vi.fn(); component.$on('submit', submitHandler); const emailInput = screen.getByLabelText(/^email$/i); await fireEvent.input(emailInput, { target: { value: 'test@example.com' } }); const passwordInput = screen.getByLabelText(/^password$/i); await fireEvent.input(passwordInput, { target: { value: 'ValidPassword123' } }); const confirmPasswordInput = screen.getByLabelText(/confirm password/i); await fireEvent.input(confirmPasswordInput, { target: { value: 'ValidPassword123' } }); const button = screen.getByRole('button', { name: /create account/i }); await fireEvent.click(button); await waitFor(() => { expect(submitHandler).toHaveBeenCalled(); }); const event = submitHandler.mock.calls[0][0]; expect(event.detail).not.toHaveProperty('confirmPassword'); }); it('does not dispatch submit event when form is invalid', async () => { const { component } = render(RegisterForm); const submitHandler = vi.fn(); component.$on('submit', submitHandler); // Try to submit with empty fields const button = screen.getByRole('button', { name: /create account/i }); await fireEvent.click(button); await waitFor(() => { expect(screen.getByText(/email is required/i)).toBeInTheDocument(); }); expect(submitHandler).not.toHaveBeenCalled(); }); it('disables all inputs when loading', () => { render(RegisterForm, { props: { isLoading: true } }); const emailInput = screen.getByLabelText(/^email$/i); const passwordInput = screen.getByLabelText(/^password$/i); const confirmPasswordInput = screen.getByLabelText(/confirm password/i); const button = screen.getByRole('button'); expect(emailInput).toBeDisabled(); expect(passwordInput).toBeDisabled(); expect(confirmPasswordInput).toBeDisabled(); expect(button).toBeDisabled(); }); }); describe('User Experience', () => { it('hides help text when password error is shown', async () => { render(RegisterForm); // Help text should be visible initially expect( screen.getByText(/must be 8\+ characters with uppercase, lowercase, and number/i) ).toBeInTheDocument(); // Enter invalid password const passwordInput = screen.getByLabelText(/^password$/i); await fireEvent.input(passwordInput, { target: { value: 'short' } }); const button = screen.getByRole('button', { name: /create account/i }); await fireEvent.click(button); // Error should be shown expect(await screen.findByText(/at least 8 characters/i)).toBeInTheDocument(); // Help text should be hidden expect( screen.queryByText(/must be 8\+ characters with uppercase, lowercase, and number/i) ).not.toBeInTheDocument(); }); it('validates all fields independently', async () => { render(RegisterForm); const button = screen.getByRole('button', { name: /create account/i }); await fireEvent.click(button); // All errors should be shown expect(await screen.findByText(/email is required/i)).toBeInTheDocument(); expect(await screen.findByText(/password is required/i)).toBeInTheDocument(); expect(await screen.findByText(/confirm your password/i)).toBeInTheDocument(); }); }); });