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()
+ })
+ })
})