feat: Add comprehensive test suite and misc improvements
- Add frontend unit tests with Vitest for components, hooks, pages, and utilities - Add backend tests for webhooks, notifications, middleware, and edge cases - Add ForgotPassword, NotFound, and ResetPassword pages - Add migration for orphaned staff resources conversion - Add coverage directory to gitignore (generated reports) - Various bug fixes and improvements from previous work 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
756
frontend/src/pages/__tests__/VerifyEmail.test.tsx
Normal file
756
frontend/src/pages/__tests__/VerifyEmail.test.tsx
Normal file
@@ -0,0 +1,756 @@
|
||||
/**
|
||||
* Unit tests for VerifyEmail component
|
||||
*
|
||||
* Tests all verification functionality including:
|
||||
* - Loading state while verifying
|
||||
* - Success message on verification
|
||||
* - Error state for invalid token
|
||||
* - Redirect after success
|
||||
* - Already verified state
|
||||
* - Missing token handling
|
||||
* - Navigation flows
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { MemoryRouter, Routes, Route } from 'react-router-dom';
|
||||
import VerifyEmail from '../VerifyEmail';
|
||||
import apiClient from '../../api/client';
|
||||
|
||||
// Mock the API client
|
||||
vi.mock('../../api/client');
|
||||
|
||||
// Mock the cookies utility
|
||||
vi.mock('../../utils/cookies', () => ({
|
||||
deleteCookie: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock the domain utility
|
||||
vi.mock('../../utils/domain', () => ({
|
||||
getBaseDomain: vi.fn(() => 'lvh.me'),
|
||||
}));
|
||||
|
||||
// Helper to render component with router and search params
|
||||
const renderWithRouter = (searchParams: string = '') => {
|
||||
const TestComponent = () => (
|
||||
<MemoryRouter initialEntries={[`/verify-email${searchParams}`]}>
|
||||
<Routes>
|
||||
<Route path="/verify-email" element={<VerifyEmail />} />
|
||||
<Route path="/login" element={<div>Login Page</div>} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
return render(<TestComponent />);
|
||||
};
|
||||
|
||||
describe('VerifyEmail', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Reset location mock
|
||||
delete (window as any).location;
|
||||
(window as any).location = {
|
||||
protocol: 'http:',
|
||||
hostname: 'platform.lvh.me',
|
||||
port: '5173',
|
||||
href: 'http://platform.lvh.me:5173/verify-email',
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('Missing Token Handling', () => {
|
||||
it('should show error when no token is provided', () => {
|
||||
renderWithRouter('');
|
||||
|
||||
expect(screen.getByText('Invalid Link')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('No verification token was provided. Please check your email for the correct link.')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render "Go to Login" button when token is missing', () => {
|
||||
renderWithRouter('');
|
||||
|
||||
const loginButton = screen.getByRole('button', { name: /go to login/i });
|
||||
expect(loginButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should navigate to login when button clicked with missing token', async () => {
|
||||
renderWithRouter('');
|
||||
|
||||
const loginButton = screen.getByRole('button', { name: /go to login/i });
|
||||
fireEvent.click(loginButton);
|
||||
|
||||
// Wait for navigation to complete
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Login Page')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error icon for missing token', () => {
|
||||
const { container } = renderWithRouter('');
|
||||
|
||||
// Check for red error styling
|
||||
const errorIcon = container.querySelector('.bg-red-100');
|
||||
expect(errorIcon).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pending State', () => {
|
||||
it('should show pending state with verify button when token is present', () => {
|
||||
renderWithRouter('?token=valid-token-123');
|
||||
|
||||
expect(screen.getByText('Verify Your Email')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Click the button below to confirm your email address.')
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /confirm verification/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show shield icon in pending state', () => {
|
||||
const { container } = renderWithRouter('?token=valid-token-123');
|
||||
|
||||
// Check for brand color styling
|
||||
const iconContainer = container.querySelector('.bg-brand-100');
|
||||
expect(iconContainer).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Loading State', () => {
|
||||
it('should show loading state while verifying', async () => {
|
||||
// Mock API to never resolve (simulating slow response)
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockImplementation(() => new Promise(() => {}));
|
||||
|
||||
renderWithRouter('?token=valid-token-123');
|
||||
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Verifying Your Email')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText('Please wait while we verify your email address...')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show loading spinner during verification', async () => {
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockImplementation(() => new Promise(() => {}));
|
||||
|
||||
const { container } = renderWithRouter('?token=valid-token-123');
|
||||
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const spinner = container.querySelector('.animate-spin');
|
||||
expect(spinner).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should call API with correct token', async () => {
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockResolvedValue({ data: { detail: 'Email verified successfully.' } });
|
||||
|
||||
renderWithRouter('?token=test-token-456');
|
||||
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPost).toHaveBeenCalledWith('/auth/email/verify/', { token: 'test-token-456' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Success State', () => {
|
||||
it('should show success message after successful verification', async () => {
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockResolvedValue({ data: { detail: 'Email verified successfully.' } });
|
||||
|
||||
renderWithRouter('?token=valid-token-123');
|
||||
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Email Verified!')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Your email address has been successfully verified. You can now sign in to your account.'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show success icon after verification', async () => {
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockResolvedValue({ data: { detail: 'Email verified successfully.' } });
|
||||
|
||||
const { container } = renderWithRouter('?token=valid-token-123');
|
||||
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const successIcon = container.querySelector('.bg-green-100');
|
||||
expect(successIcon).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should clear auth cookies after successful verification', async () => {
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockResolvedValue({ data: { detail: 'Email verified successfully.' } });
|
||||
|
||||
const { deleteCookie } = await import('../../utils/cookies');
|
||||
|
||||
renderWithRouter('?token=valid-token-123');
|
||||
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(deleteCookie).toHaveBeenCalledWith('access_token');
|
||||
expect(deleteCookie).toHaveBeenCalledWith('refresh_token');
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect to login page on success button click', async () => {
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockResolvedValue({ data: { detail: 'Email verified successfully.' } });
|
||||
|
||||
renderWithRouter('?token=valid-token-123');
|
||||
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Email Verified!')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const loginButton = screen.getByRole('button', { name: /go to login/i });
|
||||
|
||||
// Mock window.location.href to track the redirect
|
||||
let redirectUrl = '';
|
||||
Object.defineProperty((window as any).location, 'href', {
|
||||
set: (url: string) => {
|
||||
redirectUrl = url;
|
||||
},
|
||||
get: () => redirectUrl || 'http://platform.lvh.me:5173/verify-email',
|
||||
});
|
||||
|
||||
fireEvent.click(loginButton);
|
||||
|
||||
// Verify the redirect URL was set correctly
|
||||
expect(redirectUrl).toBe('http://lvh.me:5173/login');
|
||||
});
|
||||
|
||||
it('should build correct redirect URL with base domain', async () => {
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockResolvedValue({ data: { detail: 'Email verified successfully.' } });
|
||||
|
||||
renderWithRouter('?token=valid-token-123');
|
||||
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Email Verified!')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const loginButton = screen.getByRole('button', { name: /go to login/i });
|
||||
|
||||
// Mock window.location.href to track the redirect
|
||||
let redirectUrl = '';
|
||||
Object.defineProperty((window as any).location, 'href', {
|
||||
set: (url: string) => {
|
||||
redirectUrl = url;
|
||||
},
|
||||
get: () => redirectUrl || 'http://platform.lvh.me:5173/verify-email',
|
||||
});
|
||||
|
||||
fireEvent.click(loginButton);
|
||||
|
||||
// Verify the redirect URL uses the base domain
|
||||
expect(redirectUrl).toContain('lvh.me');
|
||||
expect(redirectUrl).toContain('/login');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Already Verified State', () => {
|
||||
it('should show already verified message when email is already verified', async () => {
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockResolvedValue({ data: { detail: 'Email already verified.' } });
|
||||
|
||||
renderWithRouter('?token=already-verified-token');
|
||||
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Already Verified')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
'This email address has already been verified. You can use it to sign in to your account.'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show mail icon for already verified state', async () => {
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockResolvedValue({ data: { detail: 'Email already verified.' } });
|
||||
|
||||
const { container } = renderWithRouter('?token=already-verified-token');
|
||||
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const iconContainer = container.querySelector('.bg-blue-100');
|
||||
expect(iconContainer).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should navigate to login from already verified state', async () => {
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockResolvedValue({ data: { detail: 'Email already verified.' } });
|
||||
|
||||
renderWithRouter('?token=already-verified-token');
|
||||
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Already Verified')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const loginButton = screen.getByRole('button', { name: /go to login/i });
|
||||
expect(loginButton).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error State', () => {
|
||||
it('should show error message for invalid token', async () => {
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockRejectedValue({
|
||||
response: {
|
||||
data: {
|
||||
error: 'Invalid verification token',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
renderWithRouter('?token=invalid-token');
|
||||
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Verification Failed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText('Invalid verification token')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show default error message when no error detail provided', async () => {
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockRejectedValue({
|
||||
response: {
|
||||
data: {},
|
||||
},
|
||||
});
|
||||
|
||||
renderWithRouter('?token=invalid-token');
|
||||
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Verification Failed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText('Failed to verify email')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show error icon for failed verification', async () => {
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockRejectedValue({
|
||||
response: {
|
||||
data: {
|
||||
error: 'Token expired',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { container } = renderWithRouter('?token=expired-token');
|
||||
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const errorIcon = container.querySelector('.bg-red-100');
|
||||
expect(errorIcon).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show helpful message about requesting new link', async () => {
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockRejectedValue({
|
||||
response: {
|
||||
data: {
|
||||
error: 'Token expired',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
renderWithRouter('?token=expired-token');
|
||||
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(
|
||||
'If you need a new verification link, please sign in and request one from your profile settings.'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should navigate to login from error state', async () => {
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockRejectedValue({
|
||||
response: {
|
||||
data: {
|
||||
error: 'Token expired',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
renderWithRouter('?token=expired-token');
|
||||
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Verification Failed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const loginButton = screen.getByRole('button', { name: /go to login/i });
|
||||
expect(loginButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle network errors gracefully', async () => {
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
renderWithRouter('?token=test-token');
|
||||
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Verification Failed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText('Failed to verify email')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty token parameter', () => {
|
||||
renderWithRouter('?token=');
|
||||
|
||||
expect(screen.getByText('Invalid Link')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle multiple query parameters', () => {
|
||||
renderWithRouter('?token=test-token&utm_source=email&extra=param');
|
||||
|
||||
expect(screen.getByText('Verify Your Email')).toBeInTheDocument();
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
expect(verifyButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle very long tokens', async () => {
|
||||
const longToken = 'a'.repeat(500);
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockResolvedValue({ data: { detail: 'Email verified successfully.' } });
|
||||
|
||||
renderWithRouter(`?token=${longToken}`);
|
||||
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPost).toHaveBeenCalledWith('/auth/email/verify/', { token: longToken });
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle special characters in token', async () => {
|
||||
const specialToken = 'token-with-special_chars.123+abc=xyz';
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockResolvedValue({ data: { detail: 'Email verified successfully.' } });
|
||||
|
||||
renderWithRouter(`?token=${encodeURIComponent(specialToken)}`);
|
||||
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPost).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not allow multiple simultaneous verification attempts', async () => {
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
let resolvePromise: (value: any) => void;
|
||||
const pendingPromise = new Promise((resolve) => {
|
||||
resolvePromise = resolve;
|
||||
});
|
||||
mockPost.mockReturnValue(pendingPromise as any);
|
||||
|
||||
renderWithRouter('?token=test-token');
|
||||
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
|
||||
// Click multiple times
|
||||
fireEvent.click(verifyButton);
|
||||
fireEvent.click(verifyButton);
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Verifying Your Email')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// API should only be called once
|
||||
expect(mockPost).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Resolve the promise
|
||||
resolvePromise!({ data: { detail: 'Email verified successfully.' } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Redirect URL Construction', () => {
|
||||
it('should use correct protocol in redirect URL', async () => {
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockResolvedValue({ data: { detail: 'Email verified successfully.' } });
|
||||
|
||||
(window as any).location.protocol = 'https:';
|
||||
|
||||
renderWithRouter('?token=test-token');
|
||||
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Email Verified!')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Protocol should be used in redirect construction
|
||||
expect((window as any).location.protocol).toBe('https:');
|
||||
});
|
||||
|
||||
it('should include port in redirect URL when present', async () => {
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockResolvedValue({ data: { detail: 'Email verified successfully.' } });
|
||||
|
||||
(window as any).location.port = '3000';
|
||||
|
||||
renderWithRouter('?token=test-token');
|
||||
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Email Verified!')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect((window as any).location.port).toBe('3000');
|
||||
});
|
||||
|
||||
it('should handle empty port correctly', async () => {
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockResolvedValue({ data: { detail: 'Email verified successfully.' } });
|
||||
|
||||
(window as any).location.port = '';
|
||||
|
||||
renderWithRouter('?token=test-token');
|
||||
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Email Verified!')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect((window as any).location.port).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Complete User Flows', () => {
|
||||
it('should support complete successful verification flow', async () => {
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockResolvedValue({ data: { detail: 'Email verified successfully.' } });
|
||||
|
||||
const { deleteCookie } = await import('../../utils/cookies');
|
||||
|
||||
renderWithRouter('?token=complete-flow-token');
|
||||
|
||||
// Initial state - show verify button
|
||||
expect(screen.getByText('Verify Your Email')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /confirm verification/i })).toBeInTheDocument();
|
||||
|
||||
// User clicks verify
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
// Loading state
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Verifying Your Email')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Success state
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Email Verified!')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Verify cookies were cleared
|
||||
expect(deleteCookie).toHaveBeenCalledWith('access_token');
|
||||
expect(deleteCookie).toHaveBeenCalledWith('refresh_token');
|
||||
|
||||
// User can navigate to login
|
||||
const loginButton = screen.getByRole('button', { name: /go to login/i });
|
||||
expect(loginButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should support complete failed verification flow', async () => {
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockRejectedValue({
|
||||
response: {
|
||||
data: {
|
||||
error: 'Verification token has expired',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
renderWithRouter('?token=expired-flow-token');
|
||||
|
||||
// Initial state
|
||||
expect(screen.getByText('Verify Your Email')).toBeInTheDocument();
|
||||
|
||||
// User clicks verify
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
// Loading state
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Verifying Your Email')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Error state
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Verification Failed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText('Verification token has expired')).toBeInTheDocument();
|
||||
|
||||
// User can navigate to login
|
||||
const loginButton = screen.getByRole('button', { name: /go to login/i });
|
||||
expect(loginButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should support already verified user flow', async () => {
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockResolvedValue({ data: { detail: 'Email already verified.' } });
|
||||
|
||||
renderWithRouter('?token=already-verified-flow');
|
||||
|
||||
// User clicks verify
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
// Already verified state
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Already Verified')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
'This email address has already been verified. You can use it to sign in to your account.'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
|
||||
// User can navigate to login
|
||||
const loginButton = screen.getByRole('button', { name: /go to login/i });
|
||||
expect(loginButton).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have proper heading hierarchy', () => {
|
||||
renderWithRouter('?token=test-token');
|
||||
|
||||
const heading = screen.getByRole('heading', { name: /verify your email/i });
|
||||
expect(heading).toBeInTheDocument();
|
||||
expect(heading.tagName).toBe('H1');
|
||||
});
|
||||
|
||||
it('should have accessible buttons', () => {
|
||||
renderWithRouter('?token=test-token');
|
||||
|
||||
const button = screen.getByRole('button', { name: /confirm verification/i });
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveClass('bg-brand-500');
|
||||
});
|
||||
|
||||
it('should maintain focus on interactive elements', async () => {
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockResolvedValue({ data: { detail: 'Email verified successfully.' } });
|
||||
|
||||
renderWithRouter('?token=test-token');
|
||||
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Email Verified!')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const loginButton = screen.getByRole('button', { name: /go to login/i });
|
||||
expect(loginButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have visible text for all states', async () => {
|
||||
const mockPost = vi.mocked(apiClient.post);
|
||||
mockPost.mockResolvedValue({ data: { detail: 'Email verified successfully.' } });
|
||||
|
||||
renderWithRouter('?token=test-token');
|
||||
|
||||
// Pending state has text
|
||||
expect(screen.getByText('Verify Your Email')).toBeInTheDocument();
|
||||
|
||||
const verifyButton = screen.getByRole('button', { name: /confirm verification/i });
|
||||
fireEvent.click(verifyButton);
|
||||
|
||||
// Loading state has text
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Verifying Your Email')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Success state has text
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Email Verified!')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user