phase 3.2 & 4.1

This commit is contained in:
Danilo Reyes
2025-11-02 00:36:32 -06:00
parent cac1db0ed7
commit d40139822d
21 changed files with 2230 additions and 123 deletions

View File

@@ -0,0 +1,505 @@
/**
* Component tests for authentication forms
* Tests LoginForm and RegisterForm Svelte components
*/
import { render, fireEvent, screen, waitFor } from '@testing-library/svelte';
import { describe, it, expect, vi } from 'vitest';
import LoginForm from '$lib/components/auth/LoginForm.svelte';
import RegisterForm from '$lib/components/auth/RegisterForm.svelte';
describe('LoginForm', () => {
describe('Rendering', () => {
it('renders email and password fields', () => {
render(LoginForm);
expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
});
it('renders submit button with correct text', () => {
render(LoginForm);
const button = screen.getByRole('button', { name: /login/i });
expect(button).toBeInTheDocument();
expect(button).not.toBeDisabled();
});
it('shows loading state when isLoading prop is true', () => {
render(LoginForm, { props: { isLoading: true } });
const button = screen.getByRole('button');
expect(button).toBeDisabled();
expect(screen.getByText(/logging in/i)).toBeInTheDocument();
});
it('has proper autocomplete attributes', () => {
render(LoginForm);
const emailInput = screen.getByLabelText(/email/i);
const passwordInput = screen.getByLabelText(/password/i);
expect(emailInput).toHaveAttribute('autocomplete', 'email');
expect(passwordInput).toHaveAttribute('autocomplete', 'current-password');
});
});
describe('Validation', () => {
it('shows error when email is empty on submit', async () => {
render(LoginForm);
const button = screen.getByRole('button', { name: /login/i });
await fireEvent.click(button);
expect(await screen.findByText(/email is required/i)).toBeInTheDocument();
});
it('shows error when email is invalid', async () => {
render(LoginForm);
const emailInput = screen.getByLabelText(/email/i);
await fireEvent.input(emailInput, { target: { value: 'invalid-email' } });
const button = screen.getByRole('button', { name: /login/i });
await fireEvent.click(button);
expect(await screen.findByText(/valid email address/i)).toBeInTheDocument();
});
it('shows error when password is empty on submit', async () => {
render(LoginForm);
const emailInput = screen.getByLabelText(/email/i);
await fireEvent.input(emailInput, { target: { value: 'test@example.com' } });
const button = screen.getByRole('button', { name: /login/i });
await fireEvent.click(button);
expect(await screen.findByText(/password is required/i)).toBeInTheDocument();
});
it('accepts valid email formats', async () => {
const validEmails = ['test@example.com', 'user+tag@domain.co.uk', 'first.last@example.com'];
for (const email of validEmails) {
const { unmount } = render(LoginForm);
const emailInput = screen.getByLabelText(/email/i);
await fireEvent.input(emailInput, { target: { value: email } });
const passwordInput = screen.getByLabelText(/password/i);
await fireEvent.input(passwordInput, { target: { value: 'password123' } });
const button = screen.getByRole('button', { name: /login/i });
await fireEvent.click(button);
// Should not show email error
expect(screen.queryByText(/valid email address/i)).not.toBeInTheDocument();
unmount();
}
});
it('clears errors when form is corrected', async () => {
render(LoginForm);
// Submit with empty email
const button = screen.getByRole('button', { name: /login/i });
await fireEvent.click(button);
expect(await screen.findByText(/email is required/i)).toBeInTheDocument();
// Fix email
const emailInput = screen.getByLabelText(/email/i);
await fireEvent.input(emailInput, { target: { value: 'test@example.com' } });
// Submit again
await fireEvent.click(button);
// Email error should be gone, but password error should appear
expect(screen.queryByText(/email is required/i)).not.toBeInTheDocument();
expect(await screen.findByText(/password is required/i)).toBeInTheDocument();
});
});
describe('Submission', () => {
it('dispatches submit event with correct data on valid form', async () => {
const { component } = render(LoginForm);
const submitHandler = vi.fn();
component.$on('submit', submitHandler);
const emailInput = screen.getByLabelText(/email/i);
await fireEvent.input(emailInput, { target: { value: 'test@example.com' } });
const passwordInput = screen.getByLabelText(/password/i);
await fireEvent.input(passwordInput, { target: { value: 'TestPassword123' } });
const button = screen.getByRole('button', { name: /login/i });
await fireEvent.click(button);
await waitFor(() => {
expect(submitHandler).toHaveBeenCalledTimes(1);
});
const event = submitHandler.mock.calls[0][0];
expect(event.detail).toEqual({
email: 'test@example.com',
password: 'TestPassword123',
});
});
it('does not dispatch submit event when form is invalid', async () => {
const { component } = render(LoginForm);
const submitHandler = vi.fn();
component.$on('submit', submitHandler);
// Try to submit with empty fields
const button = screen.getByRole('button', { name: /login/i });
await fireEvent.click(button);
await waitFor(() => {
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
});
expect(submitHandler).not.toHaveBeenCalled();
});
it('disables all inputs when loading', () => {
render(LoginForm, { props: { isLoading: true } });
const emailInput = screen.getByLabelText(/email/i);
const passwordInput = screen.getByLabelText(/password/i);
const button = screen.getByRole('button');
expect(emailInput).toBeDisabled();
expect(passwordInput).toBeDisabled();
expect(button).toBeDisabled();
});
});
});
describe('RegisterForm', () => {
describe('Rendering', () => {
it('renders all required fields', () => {
render(RegisterForm);
expect(screen.getByLabelText(/^email$/i)).toBeInTheDocument();
expect(screen.getByLabelText(/^password$/i)).toBeInTheDocument();
expect(screen.getByLabelText(/confirm password/i)).toBeInTheDocument();
});
it('renders submit button with correct text', () => {
render(RegisterForm);
const button = screen.getByRole('button', { name: /create account/i });
expect(button).toBeInTheDocument();
expect(button).not.toBeDisabled();
});
it('shows password requirements help text', () => {
render(RegisterForm);
expect(
screen.getByText(/must be 8\+ characters with uppercase, lowercase, and number/i)
).toBeInTheDocument();
});
it('shows loading state when isLoading prop is true', () => {
render(RegisterForm, { props: { isLoading: true } });
const button = screen.getByRole('button');
expect(button).toBeDisabled();
expect(screen.getByText(/creating account/i)).toBeInTheDocument();
});
it('has proper autocomplete attributes', () => {
render(RegisterForm);
const emailInput = screen.getByLabelText(/^email$/i);
const passwordInput = screen.getByLabelText(/^password$/i);
const confirmPasswordInput = screen.getByLabelText(/confirm password/i);
expect(emailInput).toHaveAttribute('autocomplete', 'email');
expect(passwordInput).toHaveAttribute('autocomplete', 'new-password');
expect(confirmPasswordInput).toHaveAttribute('autocomplete', 'new-password');
});
});
describe('Email Validation', () => {
it('shows error when email is empty', async () => {
render(RegisterForm);
const button = screen.getByRole('button', { name: /create account/i });
await fireEvent.click(button);
expect(await screen.findByText(/email is required/i)).toBeInTheDocument();
});
it('shows error when email is invalid', async () => {
render(RegisterForm);
const emailInput = screen.getByLabelText(/^email$/i);
await fireEvent.input(emailInput, { target: { value: 'not-an-email' } });
const button = screen.getByRole('button', { name: /create account/i });
await fireEvent.click(button);
expect(await screen.findByText(/valid email address/i)).toBeInTheDocument();
});
});
describe('Password Strength Validation', () => {
it('shows error when password is too short', async () => {
render(RegisterForm);
const passwordInput = screen.getByLabelText(/^password$/i);
await fireEvent.input(passwordInput, { target: { value: 'Test1' } });
const button = screen.getByRole('button', { name: /create account/i });
await fireEvent.click(button);
expect(await screen.findByText(/at least 8 characters/i)).toBeInTheDocument();
});
it('shows error when password lacks uppercase letter', async () => {
render(RegisterForm);
const passwordInput = screen.getByLabelText(/^password$/i);
await fireEvent.input(passwordInput, { target: { value: 'testpassword123' } });
const button = screen.getByRole('button', { name: /create account/i });
await fireEvent.click(button);
expect(await screen.findByText(/uppercase letter/i)).toBeInTheDocument();
});
it('shows error when password lacks lowercase letter', async () => {
render(RegisterForm);
const passwordInput = screen.getByLabelText(/^password$/i);
await fireEvent.input(passwordInput, { target: { value: 'TESTPASSWORD123' } });
const button = screen.getByRole('button', { name: /create account/i });
await fireEvent.click(button);
expect(await screen.findByText(/lowercase letter/i)).toBeInTheDocument();
});
it('shows error when password lacks number', async () => {
render(RegisterForm);
const passwordInput = screen.getByLabelText(/^password$/i);
await fireEvent.input(passwordInput, { target: { value: 'TestPassword' } });
const button = screen.getByRole('button', { name: /create account/i });
await fireEvent.click(button);
expect(await screen.findByText(/contain a number/i)).toBeInTheDocument();
});
it('accepts valid password meeting all requirements', async () => {
render(RegisterForm);
const emailInput = screen.getByLabelText(/^email$/i);
await fireEvent.input(emailInput, { target: { value: 'test@example.com' } });
const passwordInput = screen.getByLabelText(/^password$/i);
await fireEvent.input(passwordInput, { target: { value: 'ValidPassword123' } });
const confirmPasswordInput = screen.getByLabelText(/confirm password/i);
await fireEvent.input(confirmPasswordInput, { target: { value: 'ValidPassword123' } });
const button = screen.getByRole('button', { name: /create account/i });
await fireEvent.click(button);
// Should not show password strength errors
expect(screen.queryByText(/at least 8 characters/i)).not.toBeInTheDocument();
expect(screen.queryByText(/uppercase letter/i)).not.toBeInTheDocument();
expect(screen.queryByText(/lowercase letter/i)).not.toBeInTheDocument();
expect(screen.queryByText(/contain a number/i)).not.toBeInTheDocument();
});
});
describe('Password Confirmation Validation', () => {
it('shows error when confirm password is empty', async () => {
render(RegisterForm);
const emailInput = screen.getByLabelText(/^email$/i);
await fireEvent.input(emailInput, { target: { value: 'test@example.com' } });
const passwordInput = screen.getByLabelText(/^password$/i);
await fireEvent.input(passwordInput, { target: { value: 'ValidPassword123' } });
const button = screen.getByRole('button', { name: /create account/i });
await fireEvent.click(button);
expect(await screen.findByText(/confirm your password/i)).toBeInTheDocument();
});
it('shows error when passwords do not match', async () => {
render(RegisterForm);
const emailInput = screen.getByLabelText(/^email$/i);
await fireEvent.input(emailInput, { target: { value: 'test@example.com' } });
const passwordInput = screen.getByLabelText(/^password$/i);
await fireEvent.input(passwordInput, { target: { value: 'ValidPassword123' } });
const confirmPasswordInput = screen.getByLabelText(/confirm password/i);
await fireEvent.input(confirmPasswordInput, { target: { value: 'DifferentPassword123' } });
const button = screen.getByRole('button', { name: /create account/i });
await fireEvent.click(button);
expect(await screen.findByText(/passwords do not match/i)).toBeInTheDocument();
});
it('accepts matching passwords', async () => {
render(RegisterForm);
const emailInput = screen.getByLabelText(/^email$/i);
await fireEvent.input(emailInput, { target: { value: 'test@example.com' } });
const passwordInput = screen.getByLabelText(/^password$/i);
await fireEvent.input(passwordInput, { target: { value: 'ValidPassword123' } });
const confirmPasswordInput = screen.getByLabelText(/confirm password/i);
await fireEvent.input(confirmPasswordInput, { target: { value: 'ValidPassword123' } });
const button = screen.getByRole('button', { name: /create account/i });
await fireEvent.click(button);
// Should not show confirmation error
expect(screen.queryByText(/passwords do not match/i)).not.toBeInTheDocument();
});
});
describe('Submission', () => {
it('dispatches submit event with correct data on valid form', async () => {
const { component } = render(RegisterForm);
const submitHandler = vi.fn();
component.$on('submit', submitHandler);
const emailInput = screen.getByLabelText(/^email$/i);
await fireEvent.input(emailInput, { target: { value: 'test@example.com' } });
const passwordInput = screen.getByLabelText(/^password$/i);
await fireEvent.input(passwordInput, { target: { value: 'ValidPassword123' } });
const confirmPasswordInput = screen.getByLabelText(/confirm password/i);
await fireEvent.input(confirmPasswordInput, { target: { value: 'ValidPassword123' } });
const button = screen.getByRole('button', { name: /create account/i });
await fireEvent.click(button);
await waitFor(() => {
expect(submitHandler).toHaveBeenCalledTimes(1);
});
const event = submitHandler.mock.calls[0][0];
expect(event.detail).toEqual({
email: 'test@example.com',
password: 'ValidPassword123',
});
});
it('does not include confirmPassword in submit event', async () => {
const { component } = render(RegisterForm);
const submitHandler = vi.fn();
component.$on('submit', submitHandler);
const emailInput = screen.getByLabelText(/^email$/i);
await fireEvent.input(emailInput, { target: { value: 'test@example.com' } });
const passwordInput = screen.getByLabelText(/^password$/i);
await fireEvent.input(passwordInput, { target: { value: 'ValidPassword123' } });
const confirmPasswordInput = screen.getByLabelText(/confirm password/i);
await fireEvent.input(confirmPasswordInput, { target: { value: 'ValidPassword123' } });
const button = screen.getByRole('button', { name: /create account/i });
await fireEvent.click(button);
await waitFor(() => {
expect(submitHandler).toHaveBeenCalled();
});
const event = submitHandler.mock.calls[0][0];
expect(event.detail).not.toHaveProperty('confirmPassword');
});
it('does not dispatch submit event when form is invalid', async () => {
const { component } = render(RegisterForm);
const submitHandler = vi.fn();
component.$on('submit', submitHandler);
// Try to submit with empty fields
const button = screen.getByRole('button', { name: /create account/i });
await fireEvent.click(button);
await waitFor(() => {
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
});
expect(submitHandler).not.toHaveBeenCalled();
});
it('disables all inputs when loading', () => {
render(RegisterForm, { props: { isLoading: true } });
const emailInput = screen.getByLabelText(/^email$/i);
const passwordInput = screen.getByLabelText(/^password$/i);
const confirmPasswordInput = screen.getByLabelText(/confirm password/i);
const button = screen.getByRole('button');
expect(emailInput).toBeDisabled();
expect(passwordInput).toBeDisabled();
expect(confirmPasswordInput).toBeDisabled();
expect(button).toBeDisabled();
});
});
describe('User Experience', () => {
it('hides help text when password error is shown', async () => {
render(RegisterForm);
// Help text should be visible initially
expect(
screen.getByText(/must be 8\+ characters with uppercase, lowercase, and number/i)
).toBeInTheDocument();
// Enter invalid password
const passwordInput = screen.getByLabelText(/^password$/i);
await fireEvent.input(passwordInput, { target: { value: 'short' } });
const button = screen.getByRole('button', { name: /create account/i });
await fireEvent.click(button);
// Error should be shown
expect(await screen.findByText(/at least 8 characters/i)).toBeInTheDocument();
// Help text should be hidden
expect(
screen.queryByText(/must be 8\+ characters with uppercase, lowercase, and number/i)
).not.toBeInTheDocument();
});
it('validates all fields independently', async () => {
render(RegisterForm);
const button = screen.getByRole('button', { name: /create account/i });
await fireEvent.click(button);
// All errors should be shown
expect(await screen.findByText(/email is required/i)).toBeInTheDocument();
expect(await screen.findByText(/password is required/i)).toBeInTheDocument();
expect(await screen.findByText(/confirm your password/i)).toBeInTheDocument();
});
});
});