- 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>
757 lines
25 KiB
TypeScript
757 lines
25 KiB
TypeScript
/**
|
|
* 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();
|
|
});
|
|
});
|
|
});
|
|
});
|