phase 3.2 & 4.1
This commit is contained in:
505
frontend/tests/components/auth.test.ts
Normal file
505
frontend/tests/components/auth.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user