import { describe, it, expect, beforeEach, vi } from 'vitest'; import { render, screen, waitFor } from '../../tests/helpers/render'; import userEvent from '@testing-library/user-event'; import { http, HttpResponse } from 'msw'; import { server } from '../../tests/helpers/msw/server'; import { resetAllStores } from '../../tests/helpers/store'; import RegisterPage from './RegisterPage'; const mockNavigate = vi.fn(); vi.mock('react-router-dom', async () => { const actual = await vi.importActual('react-router-dom'); return { ...actual, useNavigate: () => mockNavigate }; }); const USERNAME_PLACEHOLDER = 'johndoe'; const EMAIL_PLACEHOLDER = 'your@email.com'; const PASSWORD_PLACEHOLDER = 'Min. 6 characters'; const CONFIRM_PASSWORD_PLACEHOLDER = 'Repeat password'; beforeEach(() => { resetAllStores(); vi.clearAllMocks(); }); describe('RegisterPage', () => { describe('FE-PAGE-REG-001: Renders registration form with all fields', () => { it('shows username, email, password, confirm-password inputs and submit button', () => { render(); expect(screen.getByPlaceholderText(USERNAME_PLACEHOLDER)).toBeInTheDocument(); expect(screen.getByPlaceholderText(EMAIL_PLACEHOLDER)).toBeInTheDocument(); expect(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER)).toBeInTheDocument(); expect(screen.getByPlaceholderText(CONFIRM_PASSWORD_PLACEHOLDER)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /register/i })).toBeInTheDocument(); }); }); describe('FE-PAGE-REG-002: Password mismatch shows error', () => { it('displays mismatch error without calling API', async () => { const user = userEvent.setup(); render(); await user.type(screen.getByPlaceholderText(USERNAME_PLACEHOLDER), 'testuser'); await user.type(screen.getByPlaceholderText(EMAIL_PLACEHOLDER), 'test@example.com'); await user.type(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER), 'password1'); await user.type(screen.getByPlaceholderText(CONFIRM_PASSWORD_PLACEHOLDER), 'password2'); await user.click(screen.getByRole('button', { name: /^register$/i })); await waitFor(() => { expect(screen.getByText(/do not match/i)).toBeInTheDocument(); }); }); }); describe('FE-PAGE-REG-003: Password too short shows error', () => { it('displays length error when passwords are the same but too short', async () => { const user = userEvent.setup(); render(); await user.type(screen.getByPlaceholderText(USERNAME_PLACEHOLDER), 'testuser'); await user.type(screen.getByPlaceholderText(EMAIL_PLACEHOLDER), 'test@example.com'); await user.type(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER), 'abc'); await user.type(screen.getByPlaceholderText(CONFIRM_PASSWORD_PLACEHOLDER), 'abc'); await user.click(screen.getByRole('button', { name: /^register$/i })); await waitFor(() => { expect(screen.getByText(/at least 8/i)).toBeInTheDocument(); }); }); }); describe('FE-PAGE-REG-004: Successful registration navigates to /dashboard', () => { it('calls navigate("/dashboard") after successful registration', async () => { const user = userEvent.setup(); render(); await user.type(screen.getByPlaceholderText(USERNAME_PLACEHOLDER), 'testuser'); await user.type(screen.getByPlaceholderText(EMAIL_PLACEHOLDER), 'test@example.com'); await user.type(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER), 'password123'); await user.type(screen.getByPlaceholderText(CONFIRM_PASSWORD_PLACEHOLDER), 'password123'); await user.click(screen.getByRole('button', { name: /^register$/i })); await waitFor(() => { expect(mockNavigate).toHaveBeenCalledWith('/dashboard'); }); }); }); describe('FE-PAGE-REG-005: Loading state during submission', () => { it('disables submit button and shows loading text while registering', async () => { server.use( http.post('/api/auth/register', async () => { await new Promise(resolve => setTimeout(resolve, 100)); return HttpResponse.json({ user: { id: 1, username: 'newuser' } }); }), ); const user = userEvent.setup(); render(); await user.type(screen.getByPlaceholderText(USERNAME_PLACEHOLDER), 'testuser'); await user.type(screen.getByPlaceholderText(EMAIL_PLACEHOLDER), 'test@example.com'); await user.type(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER), 'password123'); await user.type(screen.getByPlaceholderText(CONFIRM_PASSWORD_PLACEHOLDER), 'password123'); await user.click(screen.getByRole('button', { name: /^register$/i })); await waitFor(() => { const btn = screen.getByRole('button', { name: /registering/i }); expect(btn).toBeDisabled(); }); }); }); describe('FE-PAGE-REG-006: API error displayed', () => { it('shows error message returned by the API', async () => { server.use( http.post('/api/auth/register', () => { return HttpResponse.json({ error: 'Username already taken' }, { status: 409 }); }), ); const user = userEvent.setup(); render(); await user.type(screen.getByPlaceholderText(USERNAME_PLACEHOLDER), 'testuser'); await user.type(screen.getByPlaceholderText(EMAIL_PLACEHOLDER), 'test@example.com'); await user.type(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER), 'password123'); await user.type(screen.getByPlaceholderText(CONFIRM_PASSWORD_PLACEHOLDER), 'password123'); await user.click(screen.getByRole('button', { name: /^register$/i })); await waitFor(() => { expect(screen.getByText('Username already taken')).toBeInTheDocument(); }); }); }); describe('FE-PAGE-REG-007: Show/hide password toggle', () => { it('toggles password input type between password and text', async () => { const user = userEvent.setup(); render(); const passwordInput = screen.getByPlaceholderText(PASSWORD_PLACEHOLDER); const confirmInput = screen.getByPlaceholderText(CONFIRM_PASSWORD_PLACEHOLDER); expect(passwordInput).toHaveAttribute('type', 'password'); expect(confirmInput).toHaveAttribute('type', 'password'); // The toggle button is the only button of type "button" (not submit) before form submission const toggleButton = screen.getByRole('button', { name: '' }); await user.click(toggleButton); expect(passwordInput).toHaveAttribute('type', 'text'); expect(confirmInput).toHaveAttribute('type', 'text'); await user.click(toggleButton); expect(passwordInput).toHaveAttribute('type', 'password'); expect(confirmInput).toHaveAttribute('type', 'password'); }); }); describe('FE-PAGE-REG-008: Link to login page is present', () => { it('renders a Sign In link pointing to /login', () => { render(); const link = screen.getByRole('link', { name: /sign in/i }); expect(link).toBeInTheDocument(); expect(link).toHaveAttribute('href', '/login'); }); }); describe('FE-PAGE-REG-009: Feature list rendered', () => { it('renders feature list items in the DOM', () => { render(); // Features are always in the DOM (hidden via CSS on mobile) expect(screen.getByText(/Unlimited trip plans/i)).toBeInTheDocument(); expect(screen.getByText(/Interactive map view/i)).toBeInTheDocument(); expect(screen.getByText(/Track reservations/i)).toBeInTheDocument(); }); }); describe('FE-PAGE-REG-010: Required attribute on username input', () => { it('username input has required attribute', () => { render(); expect(screen.getByPlaceholderText(USERNAME_PLACEHOLDER)).toBeRequired(); }); }); });