From fded54e61a7f2200240376e0008b12a1b14d3134 Mon Sep 17 00:00:00 2001 From: Felipe Cardoso Date: Sun, 2 Nov 2025 17:33:57 +0100 Subject: [PATCH] Add comprehensive tests for authentication, settings, and password reset pages - Introduced smoke tests for Login, Register, Password Reset, Password Reset Confirm, and Settings pages. - Enhanced test coverage for all dynamic imports using mocks and added Jest exclusions for non-testable Next.js files. - Added component-specific test files for better structure and maintainability. - Improved test isolation by mocking navigation, providers, and rendering contexts. --- frontend/jest.config.js | 5 +- frontend/src/app/(auth)/login/page.tsx | 1 + .../confirm/PasswordResetConfirmContent.tsx | 1 + .../src/app/(auth)/password-reset/page.tsx | 1 + frontend/src/app/(auth)/register/page.tsx | 1 + .../settings/password/page.tsx | 2 + .../settings/preferences/page.tsx | 4 +- .../(authenticated)/settings/profile/page.tsx | 2 + .../settings/sessions/page.tsx | 2 + frontend/src/app/admin/page.tsx | 2 + frontend/src/app/providers.tsx | 1 + frontend/tests/app/(auth)/login/page.test.tsx | 37 ++++ .../PasswordResetConfirmContent.test.tsx | 164 ++++++++++++++++++ .../password-reset/confirm/page.test.tsx | 26 +++ .../app/(auth)/password-reset/page.test.tsx | 37 ++++ .../tests/app/(auth)/register/page.test.tsx | 37 ++++ .../(authenticated)/settings/page.test.tsx | 25 +++ .../settings/password/page.test.tsx | 26 +++ .../settings/preferences/page.test.tsx | 26 +++ .../settings/profile/page.test.tsx | 26 +++ .../settings/sessions/page.test.tsx | 26 +++ frontend/tests/app/page.test.tsx | 70 ++++++++ frontend/tests/app/providers.test.tsx | 79 +++++++++ 23 files changed, 599 insertions(+), 2 deletions(-) create mode 100644 frontend/tests/app/(auth)/login/page.test.tsx create mode 100644 frontend/tests/app/(auth)/password-reset/confirm/PasswordResetConfirmContent.test.tsx create mode 100644 frontend/tests/app/(auth)/password-reset/confirm/page.test.tsx create mode 100644 frontend/tests/app/(auth)/password-reset/page.test.tsx create mode 100644 frontend/tests/app/(auth)/register/page.test.tsx create mode 100644 frontend/tests/app/(authenticated)/settings/page.test.tsx create mode 100644 frontend/tests/app/(authenticated)/settings/password/page.test.tsx create mode 100644 frontend/tests/app/(authenticated)/settings/preferences/page.test.tsx create mode 100644 frontend/tests/app/(authenticated)/settings/profile/page.test.tsx create mode 100644 frontend/tests/app/(authenticated)/settings/sessions/page.test.tsx create mode 100644 frontend/tests/app/page.test.tsx create mode 100644 frontend/tests/app/providers.test.tsx diff --git a/frontend/jest.config.js b/frontend/jest.config.js index 606ce98..e951e9d 100644 --- a/frontend/jest.config.js +++ b/frontend/jest.config.js @@ -25,7 +25,10 @@ const customJestConfig = { '!src/lib/api/hooks/**', // React Query hooks - tested in E2E (require API mocking) '!src/**/*.old.{js,jsx,ts,tsx}', // Old implementation files '!src/components/ui/**', // shadcn/ui components - third-party, no need to test - '!src/app/**', // Next.js app directory - layout/page files (test in E2E) + '!src/app/**/layout.{js,jsx,ts,tsx}', // Layout files - complex Next.js-specific behavior (test in E2E) + '!src/app/dev/**', // Dev pages - development tools, not production code + '!src/app/**/error.{js,jsx,ts,tsx}', // Error boundaries - tested in E2E + '!src/app/**/loading.{js,jsx,ts,tsx}', // Loading states - tested in E2E '!src/**/index.{js,jsx,ts,tsx}', // Re-export index files - no logic to test '!src/lib/utils/cn.ts', // Simple utility function from shadcn '!src/middleware.ts', // middleware.ts - no logic to test diff --git a/frontend/src/app/(auth)/login/page.tsx b/frontend/src/app/(auth)/login/page.tsx index 872a9b4..70a8214 100644 --- a/frontend/src/app/(auth)/login/page.tsx +++ b/frontend/src/app/(auth)/login/page.tsx @@ -4,6 +4,7 @@ import dynamic from 'next/dynamic'; // Code-split LoginForm - heavy with react-hook-form + validation const LoginForm = dynamic( + /* istanbul ignore next - Next.js dynamic import, tested via component */ () => import('@/components/auth/LoginForm').then((mod) => ({ default: mod.LoginForm })), { loading: () => ( diff --git a/frontend/src/app/(auth)/password-reset/confirm/PasswordResetConfirmContent.tsx b/frontend/src/app/(auth)/password-reset/confirm/PasswordResetConfirmContent.tsx index fc1c9c6..70061f6 100644 --- a/frontend/src/app/(auth)/password-reset/confirm/PasswordResetConfirmContent.tsx +++ b/frontend/src/app/(auth)/password-reset/confirm/PasswordResetConfirmContent.tsx @@ -13,6 +13,7 @@ import Link from 'next/link'; // Code-split PasswordResetConfirmForm (319 lines) const PasswordResetConfirmForm = dynamic( + /* istanbul ignore next - Next.js dynamic import, tested via component */ () => import('@/components/auth/PasswordResetConfirmForm').then((mod) => ({ default: mod.PasswordResetConfirmForm })), { loading: () => ( diff --git a/frontend/src/app/(auth)/password-reset/page.tsx b/frontend/src/app/(auth)/password-reset/page.tsx index ef8ebbe..c06abfe 100644 --- a/frontend/src/app/(auth)/password-reset/page.tsx +++ b/frontend/src/app/(auth)/password-reset/page.tsx @@ -9,6 +9,7 @@ import dynamic from 'next/dynamic'; // Code-split PasswordResetRequestForm const PasswordResetRequestForm = dynamic( + /* istanbul ignore next - Next.js dynamic import, tested via component */ () => import('@/components/auth/PasswordResetRequestForm').then((mod) => ({ default: mod.PasswordResetRequestForm })), diff --git a/frontend/src/app/(auth)/register/page.tsx b/frontend/src/app/(auth)/register/page.tsx index b88d2d0..a481b96 100644 --- a/frontend/src/app/(auth)/register/page.tsx +++ b/frontend/src/app/(auth)/register/page.tsx @@ -4,6 +4,7 @@ import dynamic from 'next/dynamic'; // Code-split RegisterForm (313 lines) const RegisterForm = dynamic( + /* istanbul ignore next - Next.js dynamic import, tested via component */ () => import('@/components/auth/RegisterForm').then((mod) => ({ default: mod.RegisterForm })), { loading: () => ( diff --git a/frontend/src/app/(authenticated)/settings/password/page.tsx b/frontend/src/app/(authenticated)/settings/password/page.tsx index 06f9c9a..f793dd4 100644 --- a/frontend/src/app/(authenticated)/settings/password/page.tsx +++ b/frontend/src/app/(authenticated)/settings/password/page.tsx @@ -3,8 +3,10 @@ * Change password functionality */ +/* istanbul ignore next - Next.js type import for metadata */ import type { Metadata } from 'next'; +/* istanbul ignore next - Next.js metadata, not executable code */ export const metadata: Metadata = { title: 'Password Settings', }; diff --git a/frontend/src/app/(authenticated)/settings/preferences/page.tsx b/frontend/src/app/(authenticated)/settings/preferences/page.tsx index d7c4124..937081b 100644 --- a/frontend/src/app/(authenticated)/settings/preferences/page.tsx +++ b/frontend/src/app/(authenticated)/settings/preferences/page.tsx @@ -3,8 +3,10 @@ * Theme, notifications, and other preferences */ -import type { Metadata } from 'next'; +/* istanbul ignore next - Next.js type import for metadata */ +import type { Metadata} from 'next'; +/* istanbul ignore next - Next.js metadata, not executable code */ export const metadata: Metadata = { title: 'Preferences', }; diff --git a/frontend/src/app/(authenticated)/settings/profile/page.tsx b/frontend/src/app/(authenticated)/settings/profile/page.tsx index 0f34e73..ab7806f 100644 --- a/frontend/src/app/(authenticated)/settings/profile/page.tsx +++ b/frontend/src/app/(authenticated)/settings/profile/page.tsx @@ -3,8 +3,10 @@ * User profile management - edit name, email, phone, preferences */ +/* istanbul ignore next - Next.js type import for metadata */ import type { Metadata } from 'next'; +/* istanbul ignore next - Next.js metadata, not executable code */ export const metadata: Metadata = { title: 'Profile Settings', }; diff --git a/frontend/src/app/(authenticated)/settings/sessions/page.tsx b/frontend/src/app/(authenticated)/settings/sessions/page.tsx index a654b31..49bec6c 100644 --- a/frontend/src/app/(authenticated)/settings/sessions/page.tsx +++ b/frontend/src/app/(authenticated)/settings/sessions/page.tsx @@ -3,8 +3,10 @@ * View and manage active sessions across devices */ +/* istanbul ignore next - Next.js type import for metadata */ import type { Metadata } from 'next'; +/* istanbul ignore next - Next.js metadata, not executable code */ export const metadata: Metadata = { title: 'Active Sessions', }; diff --git a/frontend/src/app/admin/page.tsx b/frontend/src/app/admin/page.tsx index a1be8bf..a281188 100644 --- a/frontend/src/app/admin/page.tsx +++ b/frontend/src/app/admin/page.tsx @@ -4,8 +4,10 @@ * Protected by AuthGuard in layout with requireAdmin=true */ +/* istanbul ignore next - Next.js type import for metadata */ import type { Metadata } from 'next'; +/* istanbul ignore next - Next.js metadata, not executable code */ export const metadata: Metadata = { title: 'Admin Dashboard', }; diff --git a/frontend/src/app/providers.tsx b/frontend/src/app/providers.tsx index 905a550..deba5ac 100644 --- a/frontend/src/app/providers.tsx +++ b/frontend/src/app/providers.tsx @@ -7,6 +7,7 @@ import { AuthInitializer } from '@/components/auth'; // Lazy load devtools - only in local development (not in Docker), never in production // Set NEXT_PUBLIC_ENABLE_DEVTOOLS=true in .env.local to enable +/* istanbul ignore next - Dev-only devtools, not tested in production */ const ReactQueryDevtools = process.env.NODE_ENV === 'development' && process.env.NEXT_PUBLIC_ENABLE_DEVTOOLS === 'true' diff --git a/frontend/tests/app/(auth)/login/page.test.tsx b/frontend/tests/app/(auth)/login/page.test.tsx new file mode 100644 index 0000000..486b60f --- /dev/null +++ b/frontend/tests/app/(auth)/login/page.test.tsx @@ -0,0 +1,37 @@ +/** + * Tests for Login Page + * Smoke tests to verify page structure and component rendering + */ + +import { render, screen } from '@testing-library/react'; +import LoginPage from '@/app/(auth)/login/page'; + +// Mock dynamic import +jest.mock('next/dynamic', () => ({ + __esModule: true, + default: (importFn: () => Promise, options?: any) => { + const Component = () =>
Mocked LoginForm
; + Component.displayName = 'LoginForm'; + return Component; + }, +})); + +describe('LoginPage', () => { + it('renders without crashing', () => { + render(); + expect(screen.getByText('Sign in to your account')).toBeInTheDocument(); + }); + + it('renders heading and description', () => { + render(); + + expect(screen.getByRole('heading', { name: /sign in to your account/i })).toBeInTheDocument(); + expect(screen.getByText(/access your dashboard and manage your account/i)).toBeInTheDocument(); + }); + + it('renders LoginForm component', () => { + render(); + + expect(screen.getByTestId('login-form')).toBeInTheDocument(); + }); +}); diff --git a/frontend/tests/app/(auth)/password-reset/confirm/PasswordResetConfirmContent.test.tsx b/frontend/tests/app/(auth)/password-reset/confirm/PasswordResetConfirmContent.test.tsx new file mode 100644 index 0000000..bf6e08d --- /dev/null +++ b/frontend/tests/app/(auth)/password-reset/confirm/PasswordResetConfirmContent.test.tsx @@ -0,0 +1,164 @@ +/** + * Tests for Password Reset Confirm Content Component + * Verifies token validation and form rendering + */ + +import { render, screen, act } from '@testing-library/react'; +import { useSearchParams, useRouter } from 'next/navigation'; +import PasswordResetConfirmContent from '@/app/(auth)/password-reset/confirm/PasswordResetConfirmContent'; + +// Mock Next.js navigation +jest.mock('next/navigation', () => ({ + useSearchParams: jest.fn(), + useRouter: jest.fn(), + default: jest.fn(), +})); + +// Mock Next.js Link +jest.mock('next/link', () => ({ + __esModule: true, + default: ({ children, href }: { children: React.ReactNode; href: string }) => ( + {children} + ), +})); + +// Mock dynamic import +jest.mock('next/dynamic', () => ({ + __esModule: true, + default: (importFn: () => Promise, options?: any) => { + const Component = ({ onSuccess }: { onSuccess?: () => void }) => ( +
+ +
+ ); + Component.displayName = 'PasswordResetConfirmForm'; + return Component; + }, +})); + +// Mock Alert component +jest.mock('@/components/ui/alert', () => ({ + Alert: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), +})); + +describe('PasswordResetConfirmContent', () => { + let mockPush: jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); + + mockPush = jest.fn(); + (useRouter as jest.Mock).mockReturnValue({ + push: mockPush, + }); + }); + + afterEach(() => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + }); + + describe('With valid token', () => { + beforeEach(() => { + (useSearchParams as jest.Mock).mockReturnValue({ + get: jest.fn((key: string) => (key === 'token' ? 'valid-token-123' : null)), + }); + }); + + it('renders without crashing', () => { + render(); + expect(screen.getByText('Set new password')).toBeInTheDocument(); + }); + + it('renders heading and description', () => { + render(); + + expect(screen.getByRole('heading', { name: /set new password/i })).toBeInTheDocument(); + expect(screen.getByText(/choose a strong password/i)).toBeInTheDocument(); + }); + + it('renders PasswordResetConfirmForm with token', () => { + render(); + + expect(screen.getByTestId('password-reset-confirm-form')).toBeInTheDocument(); + }); + + it('redirects to login after successful password reset', () => { + render(); + + const submitButton = screen.getByRole('button', { name: /submit/i }); + + // Trigger success handler + act(() => { + submitButton.click(); + }); + + // Fast-forward time by 3 seconds + act(() => { + jest.advanceTimersByTime(3000); + }); + + expect(mockPush).toHaveBeenCalledWith('/login'); + }); + + it('cleans up timeout on unmount', () => { + const { unmount } = render(); + + const submitButton = screen.getByRole('button', { name: /submit/i }); + + // Trigger success handler + act(() => { + submitButton.click(); + }); + + // Unmount before timeout fires + unmount(); + + // Fast-forward time + act(() => { + jest.advanceTimersByTime(3000); + }); + + // Should not redirect because component was unmounted + expect(mockPush).not.toHaveBeenCalled(); + }); + }); + + describe('Without token', () => { + beforeEach(() => { + (useSearchParams as jest.Mock).mockReturnValue({ + get: jest.fn(() => null), + }); + }); + + it('shows invalid reset link error', () => { + render(); + + expect(screen.getByRole('heading', { name: /invalid reset link/i })).toBeInTheDocument(); + expect(screen.getByTestId('alert')).toBeInTheDocument(); + }); + + it('shows error message', () => { + render(); + + expect(screen.getByText(/this password reset link is invalid or has expired/i)).toBeInTheDocument(); + }); + + it('shows link to request new reset', () => { + render(); + + const link = screen.getByRole('link', { name: /request new reset link/i }); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', '/password-reset'); + }); + + it('does not render form when token is missing', () => { + render(); + + expect(screen.queryByTestId('password-reset-confirm-form')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/frontend/tests/app/(auth)/password-reset/confirm/page.test.tsx b/frontend/tests/app/(auth)/password-reset/confirm/page.test.tsx new file mode 100644 index 0000000..6f92406 --- /dev/null +++ b/frontend/tests/app/(auth)/password-reset/confirm/page.test.tsx @@ -0,0 +1,26 @@ +/** + * Tests for Password Reset Confirm Page + * Verifies Suspense wrapper and fallback + */ + +import { render, screen } from '@testing-library/react'; +import PasswordResetConfirmPage from '@/app/(auth)/password-reset/confirm/page'; + +// Mock the content component +jest.mock('@/app/(auth)/password-reset/confirm/PasswordResetConfirmContent', () => ({ + __esModule: true, + default: () =>
Content
, +})); + +describe('PasswordResetConfirmPage', () => { + it('renders without crashing', () => { + render(); + expect(screen.getByTestId('password-reset-confirm-content')).toBeInTheDocument(); + }); + + it('wraps content in Suspense boundary', () => { + render(); + // Content should render successfully (not fallback) + expect(screen.getByTestId('password-reset-confirm-content')).toBeInTheDocument(); + }); +}); diff --git a/frontend/tests/app/(auth)/password-reset/page.test.tsx b/frontend/tests/app/(auth)/password-reset/page.test.tsx new file mode 100644 index 0000000..041a504 --- /dev/null +++ b/frontend/tests/app/(auth)/password-reset/page.test.tsx @@ -0,0 +1,37 @@ +/** + * Tests for Password Reset Page + * Smoke tests to verify page structure and component rendering + */ + +import { render, screen } from '@testing-library/react'; +import PasswordResetPage from '@/app/(auth)/password-reset/page'; + +// Mock dynamic import +jest.mock('next/dynamic', () => ({ + __esModule: true, + default: (importFn: () => Promise, options?: any) => { + const Component = () =>
Mocked PasswordResetRequestForm
; + Component.displayName = 'PasswordResetRequestForm'; + return Component; + }, +})); + +describe('PasswordResetPage', () => { + it('renders without crashing', () => { + render(); + expect(screen.getByText('Reset your password')).toBeInTheDocument(); + }); + + it('renders heading and description', () => { + render(); + + expect(screen.getByRole('heading', { name: /reset your password/i })).toBeInTheDocument(); + expect(screen.getByText(/we'll send you an email with instructions/i)).toBeInTheDocument(); + }); + + it('renders PasswordResetRequestForm component', () => { + render(); + + expect(screen.getByTestId('password-reset-form')).toBeInTheDocument(); + }); +}); diff --git a/frontend/tests/app/(auth)/register/page.test.tsx b/frontend/tests/app/(auth)/register/page.test.tsx new file mode 100644 index 0000000..e0e212a --- /dev/null +++ b/frontend/tests/app/(auth)/register/page.test.tsx @@ -0,0 +1,37 @@ +/** + * Tests for Register Page + * Smoke tests to verify page structure and component rendering + */ + +import { render, screen } from '@testing-library/react'; +import RegisterPage from '@/app/(auth)/register/page'; + +// Mock dynamic import +jest.mock('next/dynamic', () => ({ + __esModule: true, + default: (importFn: () => Promise, options?: any) => { + const Component = () =>
Mocked RegisterForm
; + Component.displayName = 'RegisterForm'; + return Component; + }, +})); + +describe('RegisterPage', () => { + it('renders without crashing', () => { + render(); + expect(screen.getByText('Create your account')).toBeInTheDocument(); + }); + + it('renders heading and description', () => { + render(); + + expect(screen.getByRole('heading', { name: /create your account/i })).toBeInTheDocument(); + expect(screen.getByText(/get started with your free account today/i)).toBeInTheDocument(); + }); + + it('renders RegisterForm component', () => { + render(); + + expect(screen.getByTestId('register-form')).toBeInTheDocument(); + }); +}); diff --git a/frontend/tests/app/(authenticated)/settings/page.test.tsx b/frontend/tests/app/(authenticated)/settings/page.test.tsx new file mode 100644 index 0000000..7764ab6 --- /dev/null +++ b/frontend/tests/app/(authenticated)/settings/page.test.tsx @@ -0,0 +1,25 @@ +/** + * Tests for Settings Index Page + * Verifies redirect behavior + */ + +import { redirect } from 'next/navigation'; +import SettingsPage from '@/app/(authenticated)/settings/page'; + +// Mock Next.js navigation - redirect throws to interrupt execution +jest.mock('next/navigation', () => ({ + redirect: jest.fn(() => { + throw new Error('NEXT_REDIRECT'); + }), +})); + +describe('SettingsPage', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('redirects to /settings/profile', () => { + expect(() => SettingsPage()).toThrow('NEXT_REDIRECT'); + expect(redirect).toHaveBeenCalledWith('/settings/profile'); + }); +}); diff --git a/frontend/tests/app/(authenticated)/settings/password/page.test.tsx b/frontend/tests/app/(authenticated)/settings/password/page.test.tsx new file mode 100644 index 0000000..528348b --- /dev/null +++ b/frontend/tests/app/(authenticated)/settings/password/page.test.tsx @@ -0,0 +1,26 @@ +/** + * Tests for Password Settings Page + * Smoke tests for placeholder page + */ + +import { render, screen } from '@testing-library/react'; +import PasswordSettingsPage from '@/app/(authenticated)/settings/password/page'; + +describe('PasswordSettingsPage', () => { + it('renders without crashing', () => { + render(); + expect(screen.getByText('Password Settings')).toBeInTheDocument(); + }); + + it('renders heading', () => { + render(); + + expect(screen.getByRole('heading', { name: /password settings/i })).toBeInTheDocument(); + }); + + it('shows placeholder text', () => { + render(); + + expect(screen.getByText(/change your password/i)).toBeInTheDocument(); + }); +}); diff --git a/frontend/tests/app/(authenticated)/settings/preferences/page.test.tsx b/frontend/tests/app/(authenticated)/settings/preferences/page.test.tsx new file mode 100644 index 0000000..a418b25 --- /dev/null +++ b/frontend/tests/app/(authenticated)/settings/preferences/page.test.tsx @@ -0,0 +1,26 @@ +/** + * Tests for Preferences Page + * Smoke tests for placeholder page + */ + +import { render, screen } from '@testing-library/react'; +import PreferencesPage from '@/app/(authenticated)/settings/preferences/page'; + +describe('PreferencesPage', () => { + it('renders without crashing', () => { + render(); + expect(screen.getByText('Preferences')).toBeInTheDocument(); + }); + + it('renders heading', () => { + render(); + + expect(screen.getByRole('heading', { name: /^preferences$/i })).toBeInTheDocument(); + }); + + it('shows placeholder text', () => { + render(); + + expect(screen.getByText(/configure your preferences/i)).toBeInTheDocument(); + }); +}); diff --git a/frontend/tests/app/(authenticated)/settings/profile/page.test.tsx b/frontend/tests/app/(authenticated)/settings/profile/page.test.tsx new file mode 100644 index 0000000..5d6f36e --- /dev/null +++ b/frontend/tests/app/(authenticated)/settings/profile/page.test.tsx @@ -0,0 +1,26 @@ +/** + * Tests for Profile Settings Page + * Smoke tests for placeholder page + */ + +import { render, screen } from '@testing-library/react'; +import ProfileSettingsPage from '@/app/(authenticated)/settings/profile/page'; + +describe('ProfileSettingsPage', () => { + it('renders without crashing', () => { + render(); + expect(screen.getByText('Profile Settings')).toBeInTheDocument(); + }); + + it('renders heading', () => { + render(); + + expect(screen.getByRole('heading', { name: /profile settings/i })).toBeInTheDocument(); + }); + + it('shows placeholder text', () => { + render(); + + expect(screen.getByText(/manage your profile information/i)).toBeInTheDocument(); + }); +}); diff --git a/frontend/tests/app/(authenticated)/settings/sessions/page.test.tsx b/frontend/tests/app/(authenticated)/settings/sessions/page.test.tsx new file mode 100644 index 0000000..81ad23b --- /dev/null +++ b/frontend/tests/app/(authenticated)/settings/sessions/page.test.tsx @@ -0,0 +1,26 @@ +/** + * Tests for Sessions Page + * Smoke tests for placeholder page + */ + +import { render, screen } from '@testing-library/react'; +import SessionsPage from '@/app/(authenticated)/settings/sessions/page'; + +describe('SessionsPage', () => { + it('renders without crashing', () => { + render(); + expect(screen.getByText('Active Sessions')).toBeInTheDocument(); + }); + + it('renders heading', () => { + render(); + + expect(screen.getByRole('heading', { name: /active sessions/i })).toBeInTheDocument(); + }); + + it('shows placeholder text', () => { + render(); + + expect(screen.getByText(/manage your active sessions/i)).toBeInTheDocument(); + }); +}); diff --git a/frontend/tests/app/page.test.tsx b/frontend/tests/app/page.test.tsx new file mode 100644 index 0000000..6160d7e --- /dev/null +++ b/frontend/tests/app/page.test.tsx @@ -0,0 +1,70 @@ +/** + * Tests for Home Page + * Smoke tests for static content + */ + +import { render, screen } from '@testing-library/react'; +import Home from '@/app/page'; + +// Mock Next.js Image component +jest.mock('next/image', () => ({ + __esModule: true, + default: (props: any) => { + // eslint-disable-next-line @next/next/no-img-element, jsx-a11y/alt-text + return ; + }, +})); + +describe('HomePage', () => { + it('renders without crashing', () => { + render(); + expect(screen.getByText(/get started by editing/i)).toBeInTheDocument(); + }); + + it('renders Next.js logo', () => { + render(); + + const logo = screen.getByAltText('Next.js logo'); + expect(logo).toBeInTheDocument(); + expect(logo).toHaveAttribute('src', '/next.svg'); + }); + + it('renders Vercel logo', () => { + render(); + + const logo = screen.getByAltText('Vercel logomark'); + expect(logo).toBeInTheDocument(); + expect(logo).toHaveAttribute('src', '/vercel.svg'); + }); + + it('has correct external links', () => { + render(); + + const deployLink = screen.getByRole('link', { name: /deploy now/i }); + expect(deployLink).toHaveAttribute('href', expect.stringContaining('vercel.com')); + expect(deployLink).toHaveAttribute('target', '_blank'); + expect(deployLink).toHaveAttribute('rel', 'noopener noreferrer'); + + const docsLink = screen.getByRole('link', { name: /read our docs/i }); + expect(docsLink).toHaveAttribute('href', expect.stringContaining('nextjs.org/docs')); + expect(docsLink).toHaveAttribute('target', '_blank'); + }); + + it('renders footer links', () => { + render(); + + expect(screen.getByRole('link', { name: /learn/i })).toBeInTheDocument(); + expect(screen.getByRole('link', { name: /examples/i })).toBeInTheDocument(); + expect(screen.getByRole('link', { name: /go to nextjs\.org/i })).toBeInTheDocument(); + }); + + it('has accessible image alt texts', () => { + render(); + + expect(screen.getByAltText('Next.js logo')).toBeInTheDocument(); + expect(screen.getByAltText('Vercel logomark')).toBeInTheDocument(); + expect(screen.getByAltText('File icon')).toBeInTheDocument(); + expect(screen.getByAltText('Window icon')).toBeInTheDocument(); + expect(screen.getByAltText('Globe icon')).toBeInTheDocument(); + }); +}); diff --git a/frontend/tests/app/providers.test.tsx b/frontend/tests/app/providers.test.tsx new file mode 100644 index 0000000..95efe91 --- /dev/null +++ b/frontend/tests/app/providers.test.tsx @@ -0,0 +1,79 @@ +/** + * Tests for Providers Component + * Verifies React Query and Theme providers are configured correctly + */ + +import { render, screen } from '@testing-library/react'; +import { Providers } from '@/app/providers'; + +// Mock components +jest.mock('@/components/theme', () => ({ + ThemeProvider: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), +})); + +jest.mock('@/components/auth', () => ({ + AuthInitializer: () =>
, +})); + +// Mock TanStack Query +jest.mock('@tanstack/react-query', () => ({ + QueryClient: jest.fn().mockImplementation(() => ({})), + QueryClientProvider: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), +})); + +describe('Providers', () => { + it('renders without crashing', () => { + render( + +
Test Content
+
+ ); + + expect(screen.getByText('Test Content')).toBeInTheDocument(); + }); + + it('wraps children with ThemeProvider', () => { + render( + +
Test Content
+
+ ); + + expect(screen.getByTestId('theme-provider')).toBeInTheDocument(); + }); + + it('wraps children with QueryClientProvider', () => { + render( + +
Test Content
+
+ ); + + expect(screen.getByTestId('query-provider')).toBeInTheDocument(); + }); + + it('renders AuthInitializer', () => { + render( + +
Test Content
+
+ ); + + expect(screen.getByTestId('auth-initializer')).toBeInTheDocument(); + }); + + it('renders children', () => { + render( + +
Child Component
+
+ ); + + expect(screen.getByTestId('test-child')).toBeInTheDocument(); + expect(screen.getByText('Child Component')).toBeInTheDocument(); + }); +});