/** * Auth Feature Flow Tests * * These tests verify routing, guards, navigation, cross-screen state, and user flows * for the auth module. They run with vitest and React Testing Library. * * Contracts are defined in apps/website/lib/types/generated * * @file apps/website/tests/flows/auth.test.tsx */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import React from 'react'; import { LoginClient } from '@/client-wrapper/LoginClient'; import { SignupClient } from '@/client-wrapper/SignupClient'; import { ForgotPasswordClient } from '@/client-wrapper/ForgotPasswordClient'; import { ResetPasswordClient } from '@/client-wrapper/ResetPasswordClient'; import type { LoginViewData } from '@/lib/builders/view-data/types/LoginViewData'; import type { SignupViewData } from '@/lib/builders/view-data/types/SignupViewData'; import type { ForgotPasswordViewData } from '@/lib/builders/view-data/types/ForgotPasswordViewData'; import type { ResetPasswordViewData } from '@/lib/builders/view-data/types/ResetPasswordViewData'; import { Result } from '@/lib/contracts/Result'; // Mock next/navigation const mockPush = vi.fn(); const mockReplace = vi.fn(); const mockRefresh = vi.fn(); const mockSearchParams = new URLSearchParams(); // Mock window.location to prevent navigation errors const originalLocation = window.location; delete (window as any).location; (window as any).location = { href: '', pathname: '/auth/login', search: '', hash: '', origin: 'http://localhost:3000', protocol: 'http:', host: 'localhost:3000', hostname: 'localhost', port: '3000', assign: vi.fn(), replace: vi.fn(), reload: vi.fn(), }; vi.mock('next/navigation', () => ({ useRouter: () => ({ push: mockPush, replace: mockReplace, refresh: mockRefresh, }), useSearchParams: () => mockSearchParams, usePathname: () => '/auth/login', })); // Mock AuthContext const mockRefreshSession = vi.fn(() => Promise.resolve()); let mockSession: any = null; vi.mock('@/components/auth/AuthContext', () => ({ useAuth: () => ({ refreshSession: mockRefreshSession, session: mockSession, }), })); // Mock mutations const mockLoginMutation = { execute: vi.fn(() => Promise.resolve(Result.ok({}))), }; const mockSignupMutation = { execute: vi.fn(() => Promise.resolve(Result.ok({}))), }; const mockForgotPasswordMutation = { execute: vi.fn(() => Promise.resolve(Result.ok({}))), }; const mockResetPasswordMutation = { execute: vi.fn(() => Promise.resolve(Result.ok({}))), }; vi.mock('@/lib/mutations/auth/LoginMutation', () => ({ LoginMutation: vi.fn().mockImplementation(() => ({ execute: (...args: any[]) => mockLoginMutation.execute(...args), })), })); vi.mock('@/lib/mutations/auth/SignupMutation', () => ({ SignupMutation: vi.fn().mockImplementation(() => ({ execute: (...args: any[]) => mockSignupMutation.execute(...args), })), })); vi.mock('@/lib/mutations/auth/ForgotPasswordMutation', () => ({ ForgotPasswordMutation: vi.fn().mockImplementation(() => ({ execute: (...args: any[]) => mockForgotPasswordMutation.execute(...args), })), })); vi.mock('@/lib/mutations/auth/ResetPasswordMutation', () => ({ ResetPasswordMutation: vi.fn().mockImplementation(() => ({ execute: (...args: any[]) => mockResetPasswordMutation.execute(...args), })), })); // Mock process.env const originalNodeEnv = process.env.NODE_ENV; beforeEach(() => { process.env.NODE_ENV = 'development'; vi.clearAllMocks(); mockSession = null; mockSearchParams.delete('returnTo'); mockSearchParams.delete('token'); }); afterEach(() => { process.env.NODE_ENV = originalNodeEnv; }); describe('Auth Feature Flow', () => { describe('Login Flow', () => { const mockLoginViewData: LoginViewData = { formState: { fields: { email: { value: '', touched: false, error: undefined, validating: false }, password: { value: '', touched: false, error: undefined, validating: false }, rememberMe: { value: false, touched: false, error: undefined, validating: false }, }, isValid: true, isSubmitting: false, submitCount: 0, submitError: undefined, }, showPassword: false, showErrorDetails: false, hasInsufficientPermissions: false, returnTo: '/dashboard', isSubmitting: false, }; it('should display login form with all fields', () => { render(); expect(screen.getByLabelText(/email address/i)).toBeDefined(); expect(screen.getByLabelText(/password/i)).toBeDefined(); expect(screen.getByLabelText(/keep me signed in/i)).toBeDefined(); expect(screen.getByRole('button', { name: /sign in/i })).toBeDefined(); }); it('should display validation errors for empty fields', async () => { render(); const submitButton = screen.getByRole('button', { name: /sign in/i }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByText(/email is required/i)).toBeDefined(); expect(screen.getByText(/password is required/i)).toBeDefined(); }); }); it('should display validation error for invalid email format', async () => { render(); const emailInput = screen.getByLabelText(/email address/i); fireEvent.change(emailInput, { target: { value: 'invalid-email' } }); const submitButton = screen.getByRole('button', { name: /sign in/i }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByText(/invalid email/i)).toBeDefined(); }); }); it('should successfully login with valid credentials', async () => { mockLoginMutation.execute.mockImplementation(() => { mockSession = { userId: 'user-123' }; return Promise.resolve(Result.ok({ token: 'test-token-123', user: { userId: 'user-123', email: 'test@example.com', displayName: 'Test User', role: 'user', }, })); }); render(); const emailInput = screen.getByLabelText(/email address/i); const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole('button', { name: /sign in/i }); fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); fireEvent.change(passwordInput, { target: { value: 'ValidPass123!' } }); fireEvent.click(submitButton); await waitFor(() => { expect(mockLoginMutation.execute).toHaveBeenCalledWith({ email: 'test@example.com', password: 'ValidPass123!', rememberMe: false, }); }); await waitFor(() => { expect(mockRefreshSession).toHaveBeenCalled(); }); await waitFor(() => { expect(mockPush).toHaveBeenCalledWith('/dashboard'); }); }); it('should handle login with remember me option', async () => { mockLoginMutation.execute.mockImplementation(() => { mockSession = { userId: 'user-123' }; return Promise.resolve(Result.ok({ token: 'test-token-123', user: { userId: 'user-123', email: 'test@example.com', displayName: 'Test User', role: 'user', }, })); }); render(); const emailInput = screen.getByLabelText(/email address/i); const passwordInput = screen.getByLabelText(/password/i); const rememberMeCheckbox = screen.getByLabelText(/keep me signed in/i); const submitButton = screen.getByRole('button', { name: /sign in/i }); fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); fireEvent.change(passwordInput, { target: { value: 'ValidPass123!' } }); fireEvent.click(rememberMeCheckbox); fireEvent.click(submitButton); await waitFor(() => { expect(mockLoginMutation.execute).toHaveBeenCalledWith({ email: 'test@example.com', password: 'ValidPass123!', rememberMe: true, }); }); }); it('should handle login errors (invalid credentials)', async () => { mockLoginMutation.execute.mockResolvedValue( Result.err('Invalid credentials') ); render(); const emailInput = screen.getByLabelText(/email address/i); const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole('button', { name: /sign in/i }); fireEvent.change(emailInput, { target: { value: 'wrong@example.com' } }); fireEvent.change(passwordInput, { target: { value: 'WrongPass123!' } }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByText(/invalid credentials/i)).toBeDefined(); }); // Verify form remains in error state expect((emailInput as HTMLInputElement).value).toBe('wrong@example.com'); }); it('should handle login errors (server error)', async () => { mockLoginMutation.execute.mockResolvedValue( Result.err('Internal Server Error') ); render(); const emailInput = screen.getByLabelText(/email address/i); const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole('button', { name: /sign in/i }); fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); fireEvent.change(passwordInput, { target: { value: 'ValidPass123!' } }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByText(/error/i)).toBeDefined(); }); }); it('should navigate to forgot password from login', () => { render(); const forgotPasswordLink = screen.getByText(/forgot password/i); fireEvent.click(forgotPasswordLink); expect(mockPush).toHaveBeenCalledWith('/auth/forgot-password'); }); it('should navigate to signup from login', () => { render(); const signupLink = screen.getByText(/create one/i); fireEvent.click(signupLink); expect(mockPush).toHaveBeenCalledWith('/auth/signup'); }); it('should handle password visibility toggle', () => { render(); const passwordInput = screen.getByLabelText(/password/i); expect((passwordInput as HTMLInputElement).type).toBe('password'); const toggleButton = screen.getByRole('button', { name: /show password/i }); fireEvent.click(toggleButton); expect((passwordInput as HTMLInputElement).type).toBe('text'); }); }); describe('Signup Flow', () => { const mockSignupViewData: SignupViewData = { returnTo: '/onboarding', formState: { fields: { firstName: { value: '', touched: false, error: undefined, validating: false }, lastName: { value: '', touched: false, error: undefined, validating: false }, email: { value: '', touched: false, error: undefined, validating: false }, password: { value: '', touched: false, error: undefined, validating: false }, confirmPassword: { value: '', touched: false, error: undefined, validating: false }, }, isValid: true, isSubmitting: false, submitCount: 0, submitError: undefined, }, isSubmitting: false, }; it('should display signup form with all fields', () => { render(); expect(screen.getByLabelText(/first name/i)).toBeDefined(); expect(screen.getByLabelText(/last name/i)).toBeDefined(); expect(screen.getByLabelText(/email address/i)).toBeDefined(); expect(screen.getByLabelText(/^password$/i)).toBeDefined(); expect(screen.getByLabelText(/confirm password/i)).toBeDefined(); expect(screen.getByRole('button', { name: /create account/i })).toBeDefined(); }); it('should display validation errors for empty required fields', async () => { render(); const submitButton = screen.getByRole('button', { name: /create account/i }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByText(/first name is required/i)).toBeDefined(); expect(screen.getByText(/last name is required/i)).toBeDefined(); expect(screen.getByText(/email is required/i)).toBeDefined(); expect(screen.getByText(/password is required/i)).toBeDefined(); expect(screen.getByText(/confirm password is required/i)).toBeDefined(); }); }); it('should display validation errors for weak password', async () => { render(); const passwordInput = screen.getByLabelText(/^password$/i); fireEvent.change(passwordInput, { target: { value: 'weak' } }); const submitButton = screen.getByRole('button', { name: /create account/i }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByText(/password must be/i)).toBeDefined(); }); }); it('should successfully signup with valid data', async () => { mockSignupMutation.execute.mockImplementation(() => { mockSession = { userId: 'user-456' }; return Promise.resolve(Result.ok({ token: 'test-token-456', user: { userId: 'user-456', email: 'newuser@example.com', displayName: 'New User', role: 'user', }, })); }); render(); const firstNameInput = screen.getByLabelText(/first name/i); const lastNameInput = screen.getByLabelText(/last name/i); const emailInput = screen.getByLabelText(/email address/i); const passwordInput = screen.getByLabelText(/^password$/i); const confirmPasswordInput = screen.getByLabelText(/confirm password/i); const submitButton = screen.getByRole('button', { name: /create account/i }); fireEvent.change(firstNameInput, { target: { value: 'New' } }); fireEvent.change(lastNameInput, { target: { value: 'User' } }); fireEvent.change(emailInput, { target: { value: 'newuser@example.com' } }); fireEvent.change(passwordInput, { target: { value: 'ValidPass123!' } }); fireEvent.change(confirmPasswordInput, { target: { value: 'ValidPass123!' } }); fireEvent.click(submitButton); await waitFor(() => { expect(mockSignupMutation.execute).toHaveBeenCalledWith({ email: 'newuser@example.com', password: 'ValidPass123!', displayName: 'New User', }); }); await waitFor(() => { expect(mockRefreshSession).toHaveBeenCalled(); }); await waitFor(() => { expect(mockPush).toHaveBeenCalledWith('/onboarding'); }); }); it('should handle signup errors (email already exists)', async () => { mockSignupMutation.execute.mockResolvedValue( Result.err('Email already exists') ); render(); const firstNameInput = screen.getByLabelText(/first name/i); const lastNameInput = screen.getByLabelText(/last name/i); const emailInput = screen.getByLabelText(/email address/i); const passwordInput = screen.getByLabelText(/^password$/i); const confirmPasswordInput = screen.getByLabelText(/confirm password/i); const submitButton = screen.getByRole('button', { name: /create account/i }); fireEvent.change(firstNameInput, { target: { value: 'Existing' } }); fireEvent.change(lastNameInput, { target: { value: 'User' } }); fireEvent.change(emailInput, { target: { value: 'existing@example.com' } }); fireEvent.change(passwordInput, { target: { value: 'ValidPass123!' } }); fireEvent.change(confirmPasswordInput, { target: { value: 'ValidPass123!' } }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByText(/already exists/i)).toBeDefined(); }); }); it('should handle signup errors (server error)', async () => { mockSignupMutation.execute.mockResolvedValue( Result.err('Internal Server Error') ); render(); const firstNameInput = screen.getByLabelText(/first name/i); const lastNameInput = screen.getByLabelText(/last name/i); const emailInput = screen.getByLabelText(/email address/i); const passwordInput = screen.getByLabelText(/^password$/i); const confirmPasswordInput = screen.getByLabelText(/confirm password/i); const submitButton = screen.getByRole('button', { name: /create account/i }); fireEvent.change(firstNameInput, { target: { value: 'Test' } }); fireEvent.change(lastNameInput, { target: { value: 'User' } }); fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); fireEvent.change(passwordInput, { target: { value: 'ValidPass123!' } }); fireEvent.change(confirmPasswordInput, { target: { value: 'ValidPass123!' } }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByText(/error/i)).toBeDefined(); }); }); it('should navigate to login from signup', () => { render(); const loginLink = screen.getByText(/already have an account/i); fireEvent.click(loginLink); expect(mockPush).toHaveBeenCalledWith('/auth/login'); }); it('should handle password visibility toggle', () => { render(); const passwordInput = screen.getByLabelText(/^password$/i); expect((passwordInput as HTMLInputElement).type).toBe('password'); const toggleButton = screen.getByRole('button', { name: /show password/i }); fireEvent.click(toggleButton); expect((passwordInput as HTMLInputElement).type).toBe('text'); }); it('should handle confirm password visibility toggle', () => { render(); const confirmPasswordInput = screen.getByLabelText(/confirm password/i); expect((confirmPasswordInput as HTMLInputElement).type).toBe('password'); const toggleButton = screen.getByRole('button', { name: /show confirm password/i }); fireEvent.click(toggleButton); expect((confirmPasswordInput as HTMLInputElement).type).toBe('text'); }); }); describe('Forgot Password Flow', () => { const mockForgotPasswordViewData: ForgotPasswordViewData = { returnTo: '/auth/login', formState: { fields: { email: { value: '', touched: false, error: undefined, validating: false }, }, isValid: true, isSubmitting: false, submitCount: 0, submitError: undefined, }, showSuccess: false, successMessage: undefined, magicLink: undefined, isSubmitting: false, }; it('should display forgot password form with email field', () => { render(); expect(screen.getByLabelText(/email address/i)).toBeDefined(); expect(screen.getByRole('button', { name: /send reset link/i })).toBeDefined(); }); it('should display validation error for empty email', async () => { render(); const submitButton = screen.getByRole('button', { name: /send reset link/i }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByText(/email is required/i)).toBeDefined(); }); }); it('should display validation error for invalid email format', async () => { render(); const emailInput = screen.getByLabelText(/email address/i); fireEvent.change(emailInput, { target: { value: 'invalid-email' } }); const submitButton = screen.getByRole('button', { name: /send reset link/i }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByText(/invalid email/i)).toBeDefined(); }); }); it('should successfully submit forgot password request', async () => { mockForgotPasswordMutation.execute.mockResolvedValue( Result.ok({ success: true, message: 'Password reset email sent', }) ); render(); const emailInput = screen.getByLabelText(/email address/i); const submitButton = screen.getByRole('button', { name: /send reset link/i }); fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); fireEvent.click(submitButton); await waitFor(() => { expect(mockForgotPasswordMutation.execute).toHaveBeenCalledWith({ email: 'test@example.com', }); }); await waitFor(() => { expect(screen.getByText(/password reset email sent/i)).toBeDefined(); }); }); it('should handle forgot password errors (email not found)', async () => { mockForgotPasswordMutation.execute.mockResolvedValue( Result.err('Email not found') ); render(); const emailInput = screen.getByLabelText(/email address/i); const submitButton = screen.getByRole('button', { name: /send reset link/i }); fireEvent.change(emailInput, { target: { value: 'nonexistent@example.com' } }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByText(/email not found/i)).toBeDefined(); }); }); it('should handle forgot password errors (rate limit)', async () => { mockForgotPasswordMutation.execute.mockResolvedValue( Result.err('Rate limit exceeded') ); render(); const emailInput = screen.getByLabelText(/email address/i); const submitButton = screen.getByRole('button', { name: /send reset link/i }); fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByText(/rate limit/i)).toBeDefined(); }); }); it('should navigate back to login from forgot password', () => { render(); const loginLink = screen.getByText(/back to login/i); fireEvent.click(loginLink); expect(mockPush).toHaveBeenCalledWith('/auth/login'); }); }); describe('Reset Password Flow', () => { const mockResetPasswordViewData: ResetPasswordViewData = { token: 'abc123', returnTo: '/auth/login', formState: { fields: { newPassword: { value: '', touched: false, error: undefined, validating: false }, confirmPassword: { value: '', touched: false, error: undefined, validating: false }, }, isValid: true, isSubmitting: false, submitCount: 0, submitError: undefined, }, showSuccess: false, successMessage: undefined, isSubmitting: false, }; beforeEach(() => { mockSearchParams.set('token', 'abc123'); }); it('should display reset password form with password fields', () => { render(); expect(screen.getByLabelText(/^new password$/i)).toBeDefined(); expect(screen.getByLabelText(/confirm password/i)).toBeDefined(); expect(screen.getByRole('button', { name: /reset password/i })).toBeDefined(); }); it('should display validation errors for empty password fields', async () => { render(); const submitButton = screen.getByRole('button', { name: /reset password/i }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByText(/new password is required/i)).toBeDefined(); expect(screen.getByText(/confirm password is required/i)).toBeDefined(); }); }); it('should display validation error for non-matching passwords', async () => { render(); const newPasswordInput = screen.getByLabelText(/^new password$/i); const confirmPasswordInput = screen.getByLabelText(/confirm password/i); fireEvent.change(newPasswordInput, { target: { value: 'ValidPass123!' } }); fireEvent.change(confirmPasswordInput, { target: { value: 'DifferentPass456!' } }); const submitButton = screen.getByRole('button', { name: /reset password/i }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByText(/passwords do not match/i)).toBeDefined(); }); }); it('should display validation error for weak new password', async () => { render(); const newPasswordInput = screen.getByLabelText(/^new password$/i); fireEvent.change(newPasswordInput, { target: { value: 'weak' } }); const submitButton = screen.getByRole('button', { name: /reset password/i }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByText(/password must be/i)).toBeDefined(); }); }); it('should successfully reset password', async () => { mockResetPasswordMutation.execute.mockResolvedValue( Result.ok({ success: true, message: 'Password reset successfully', }) ); render(); const newPasswordInput = screen.getByLabelText(/^new password$/i); const confirmPasswordInput = screen.getByLabelText(/confirm password/i); const submitButton = screen.getByRole('button', { name: /reset password/i }); fireEvent.change(newPasswordInput, { target: { value: 'NewPass123!' } }); fireEvent.change(confirmPasswordInput, { target: { value: 'NewPass123!' } }); fireEvent.click(submitButton); await waitFor(() => { expect(mockResetPasswordMutation.execute).toHaveBeenCalledWith({ token: 'abc123', newPassword: 'NewPass123!', }); }); await waitFor(() => { expect(screen.getByText(/password reset successfully/i)).toBeDefined(); }); // Verify redirect to login page after delay await waitFor(() => { expect(mockPush).toHaveBeenCalledWith('/auth/login'); }, { timeout: 5000 }); }); it('should handle reset password with invalid token', async () => { mockResetPasswordMutation.execute.mockResolvedValue( Result.err('Invalid token') ); render(); const newPasswordInput = screen.getByLabelText(/^new password$/i); const confirmPasswordInput = screen.getByLabelText(/confirm password/i); const submitButton = screen.getByRole('button', { name: /reset password/i }); fireEvent.change(newPasswordInput, { target: { value: 'NewPass123!' } }); fireEvent.change(confirmPasswordInput, { target: { value: 'NewPass123!' } }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByText(/invalid token/i)).toBeDefined(); }); // Verify form is disabled expect((submitButton as HTMLButtonElement).disabled).toBe(true); }); it('should handle reset password with expired token', async () => { mockResetPasswordMutation.execute.mockResolvedValue( Result.err('Token expired') ); render(); const newPasswordInput = screen.getByLabelText(/^new password$/i); const confirmPasswordInput = screen.getByLabelText(/confirm password/i); const submitButton = screen.getByRole('button', { name: /reset password/i }); fireEvent.change(newPasswordInput, { target: { value: 'NewPass123!' } }); fireEvent.change(confirmPasswordInput, { target: { value: 'NewPass123!' } }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByText(/token expired/i)).toBeDefined(); }); // Verify link to request new reset email expect(screen.getByText(/request new link/i)).toBeDefined(); }); it('should handle reset password errors (server error)', async () => { mockResetPasswordMutation.execute.mockResolvedValue( Result.err('Internal Server Error') ); render(); const newPasswordInput = screen.getByLabelText(/^new password$/i); const confirmPasswordInput = screen.getByLabelText(/confirm password/i); const submitButton = screen.getByRole('button', { name: /reset password/i }); fireEvent.change(newPasswordInput, { target: { value: 'NewPass123!' } }); fireEvent.change(confirmPasswordInput, { target: { value: 'NewPass123!' } }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByText(/error/i)).toBeDefined(); }); }); it('should navigate to login from reset password', () => { render(); const loginLink = screen.getByText(/back to login/i); fireEvent.click(loginLink); expect(mockPush).toHaveBeenCalledWith('/auth/login'); }); it('should handle password visibility toggle', () => { render(); const newPasswordInput = screen.getByLabelText(/^new password$/i); expect((newPasswordInput as HTMLInputElement).type).toBe('password'); const toggleButton = screen.getByRole('button', { name: /show password/i }); fireEvent.click(toggleButton); expect((newPasswordInput as HTMLInputElement).type).toBe('text'); }); it('should handle confirm password visibility toggle', () => { render(); const confirmPasswordInput = screen.getByLabelText(/confirm password/i); expect((confirmPasswordInput as HTMLInputElement).type).toBe('password'); const toggleButton = screen.getByRole('button', { name: /show confirm password/i }); fireEvent.click(toggleButton); expect((confirmPasswordInput as HTMLInputElement).type).toBe('text'); }); }); describe('Auth Cross-Screen State Management', () => { const mockLoginViewData: LoginViewData = { formState: { fields: { email: { value: '', touched: false, error: undefined, validating: false }, password: { value: '', touched: false, error: undefined, validating: false }, rememberMe: { value: false, touched: false, error: undefined, validating: false }, }, isValid: true, isSubmitting: false, submitCount: 0, submitError: undefined, }, showPassword: false, showErrorDetails: false, hasInsufficientPermissions: false, returnTo: '/dashboard', isSubmitting: false, }; it('should preserve form data when navigating between auth pages', async () => { render(); const emailInput = screen.getByLabelText(/email address/i); fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); // Navigate to forgot password const forgotPasswordLink = screen.getByText(/forgot password/i); fireEvent.click(forgotPasswordLink); // Navigate back to login const loginLink = screen.getByText(/back to login/i); fireEvent.click(loginLink); // Verify email is preserved await waitFor(() => { expect((emailInput as HTMLInputElement).value).toBe('test@example.com'); }); }); it('should clear form data after successful authentication', async () => { mockLoginMutation.execute.mockImplementation(() => { mockSession = { userId: 'user-123' }; return Promise.resolve(Result.ok({ token: 'test-token-123', user: { userId: 'user-123', email: 'test@example.com', displayName: 'Test User', role: 'user', }, })); }); render(); const emailInput = screen.getByLabelText(/email address/i); const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole('button', { name: /sign in/i }); fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); fireEvent.change(passwordInput, { target: { value: 'ValidPass123!' } }); fireEvent.click(submitButton); await waitFor(() => { expect(mockPush).toHaveBeenCalledWith('/dashboard'); }); // Navigate back to login mockPush.mockClear(); const loginLink = screen.getByText(/sign in/i); fireEvent.click(loginLink); // Verify form is cleared await waitFor(() => { expect((emailInput as HTMLInputElement).value).toBe(''); expect((passwordInput as HTMLInputElement).value).toBe(''); }); }); it('should handle concurrent auth operations', async () => { mockLoginMutation.execute.mockImplementation(() => { mockSession = { userId: 'user-123' }; return Promise.resolve(Result.ok({ token: 'test-token-123', user: { userId: 'user-123', email: 'test@example.com', displayName: 'Test User', role: 'user', }, })); }); render(); const emailInput = screen.getByLabelText(/email address/i); const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole('button', { name: /sign in/i }); fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); fireEvent.change(passwordInput, { target: { value: 'ValidPass123!' } }); // Click submit multiple times quickly fireEvent.click(submitButton); fireEvent.click(submitButton); fireEvent.click(submitButton); await waitFor(() => { expect(mockLoginMutation.execute).toHaveBeenCalledTimes(1); }); await waitFor(() => { expect(mockPush).toHaveBeenCalledWith('/dashboard'); }); }); }); describe('Auth UI State Management', () => { const mockLoginViewData: LoginViewData = { formState: { fields: { email: { value: '', touched: false, error: undefined, validating: false }, password: { value: '', touched: false, error: undefined, validating: false }, rememberMe: { value: false, touched: false, error: undefined, validating: false }, }, isValid: true, isSubmitting: false, submitCount: 0, submitError: undefined, }, showPassword: false, showErrorDetails: false, hasInsufficientPermissions: false, returnTo: '/dashboard', isSubmitting: false, }; it('should show loading states during auth operations', async () => { mockLoginMutation.execute.mockImplementation( () => new Promise(resolve => setTimeout(() => { mockSession = { userId: 'user-123' }; resolve(Result.ok({ token: 'test-token-123', user: { userId: 'user-123', email: 'test@example.com', displayName: 'Test User', role: 'user', }, })); }, 100)) ); render(); const emailInput = screen.getByLabelText(/email address/i); const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole('button', { name: /sign in/i }); fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); fireEvent.change(passwordInput, { target: { value: 'ValidPass123!' } }); fireEvent.click(submitButton); // Verify loading state await waitFor(() => { expect(screen.getByText(/signing in/i)).toBeDefined(); }); // Wait for loading to complete await waitFor(() => { expect(screen.queryByText(/signing in/i)).toBeNull(); }); // Verify authentication is successful await waitFor(() => { expect(mockPush).toHaveBeenCalledWith('/dashboard'); }); }); it('should handle error states gracefully', async () => { mockLoginMutation.execute.mockResolvedValue( Result.err('Invalid credentials') ); render(); const emailInput = screen.getByLabelText(/email address/i); const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole('button', { name: /sign in/i }); fireEvent.change(emailInput, { target: { value: 'wrong@example.com' } }); fireEvent.change(passwordInput, { target: { value: 'WrongPass123!' } }); fireEvent.click(submitButton); // Verify error message is displayed await waitFor(() => { expect(screen.getByText(/invalid credentials/i)).toBeDefined(); }); // Verify UI remains usable after errors expect((emailInput as HTMLInputElement).disabled).toBe(false); expect((passwordInput as HTMLInputElement).disabled).toBe(false); expect((submitButton as HTMLButtonElement).disabled).toBe(false); }); it('should handle network connectivity issues', async () => { mockLoginMutation.execute.mockResolvedValue( Result.err('Network Error') ); render(); const emailInput = screen.getByLabelText(/email address/i); const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole('button', { name: /sign in/i }); fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); fireEvent.change(passwordInput, { target: { value: 'ValidPass123!' } }); fireEvent.click(submitButton); // Verify network error message is shown await waitFor(() => { expect(screen.getByText(/network/i)).toBeDefined(); }); }); }); });