diff --git a/src/design-system/composites/LoginForm/LoginForm.test.tsx b/src/design-system/composites/LoginForm/LoginForm.test.tsx index 33a8f60..a6b1e88 100644 --- a/src/design-system/composites/LoginForm/LoginForm.test.tsx +++ b/src/design-system/composites/LoginForm/LoginForm.test.tsx @@ -1,5 +1,6 @@ import { describe, it, expect, vi } from 'vitest' import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import { LoginForm } from './LoginForm' const socialProviders = [ @@ -73,4 +74,120 @@ describe('LoginForm', () => { expect(screen.getByText('Invalid credentials')).toBeInTheDocument() }) }) + + describe('validation', () => { + it('validates required email', async () => { + const user = userEvent.setup() + render() + await user.click(screen.getByRole('button', { name: 'Sign in' })) + expect(screen.getByText('Email is required')).toBeInTheDocument() + }) + + it('validates email format', async () => { + const user = userEvent.setup() + render() + await user.type(screen.getByLabelText(/email/i), 'notanemail') + await user.type(screen.getByLabelText(/password/i), 'password123') + await user.click(screen.getByRole('button', { name: 'Sign in' })) + expect(screen.getByText('Please enter a valid email address')).toBeInTheDocument() + }) + + it('validates required password', async () => { + const user = userEvent.setup() + render() + await user.type(screen.getByLabelText(/email/i), 'test@example.com') + await user.click(screen.getByRole('button', { name: 'Sign in' })) + expect(screen.getByText('Password is required')).toBeInTheDocument() + }) + + it('validates password minimum length', async () => { + const user = userEvent.setup() + render() + await user.type(screen.getByLabelText(/email/i), 'test@example.com') + await user.type(screen.getByLabelText(/password/i), 'short') + await user.click(screen.getByRole('button', { name: 'Sign in' })) + expect(screen.getByText('Password must be at least 8 characters')).toBeInTheDocument() + }) + + it('clears field errors on typing', async () => { + const user = userEvent.setup() + render() + await user.click(screen.getByRole('button', { name: 'Sign in' })) + expect(screen.getByText('Email is required')).toBeInTheDocument() + await user.type(screen.getByLabelText(/email/i), 't') + expect(screen.queryByText('Email is required')).not.toBeInTheDocument() + }) + + it('calls onSubmit with credentials when valid', async () => { + const onSubmit = vi.fn() + const user = userEvent.setup() + render() + await user.type(screen.getByLabelText(/email/i), 'test@example.com') + await user.type(screen.getByLabelText(/password/i), 'password123') + await user.click(screen.getByLabelText(/remember me/i)) + await user.click(screen.getByRole('button', { name: 'Sign in' })) + expect(onSubmit).toHaveBeenCalledWith({ + email: 'test@example.com', + password: 'password123', + remember: true, + }) + }) + + it('does not call onSubmit when validation fails', async () => { + const onSubmit = vi.fn() + const user = userEvent.setup() + render() + await user.click(screen.getByRole('button', { name: 'Sign in' })) + expect(onSubmit).not.toHaveBeenCalled() + }) + }) + + describe('loading state', () => { + it('disables form inputs when loading', () => { + render() + expect(screen.getByLabelText(/email/i)).toBeDisabled() + expect(screen.getByLabelText(/password/i)).toBeDisabled() + expect(screen.getByLabelText(/remember me/i)).toBeDisabled() + }) + + it('shows spinner on submit button when loading', () => { + render() + const submitBtn = screen.getByRole('button', { name: /sign in/i }) + expect(submitBtn).toBeDisabled() + // Button component renders Spinner when loading=true + expect(submitBtn.querySelector('[class*="spinner"]')).toBeInTheDocument() + }) + + it('disables social buttons when loading', () => { + render() + expect(screen.getByRole('button', { name: 'Continue with Google' })).toBeDisabled() + expect(screen.getByRole('button', { name: 'Continue with GitHub' })).toBeDisabled() + }) + }) + + describe('callbacks', () => { + it('calls social provider onClick when clicked', async () => { + const onClick = vi.fn() + const user = userEvent.setup() + render() + await user.click(screen.getByRole('button', { name: 'Continue with Google' })) + expect(onClick).toHaveBeenCalledOnce() + }) + + it('calls onForgotPassword when link clicked', async () => { + const onForgotPassword = vi.fn() + const user = userEvent.setup() + render() + await user.click(screen.getByText(/forgot password/i)) + expect(onForgotPassword).toHaveBeenCalledOnce() + }) + + it('calls onSignUp when link clicked', async () => { + const onSignUp = vi.fn() + const user = userEvent.setup() + render() + await user.click(screen.getByText(/sign up/i)) + expect(onSignUp).toHaveBeenCalledOnce() + }) + }) })