// ABOUTME: Unit tests for the Login page component. // ABOUTME: Tests form rendering, validation, auth flow, and error handling. import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; // Mock next/navigation const mockPush = vi.fn(); vi.mock("next/navigation", () => ({ useRouter: () => ({ push: mockPush, }), })); // Mock PocketBase const mockAuthWithPassword = vi.fn(); vi.mock("@/lib/pocketbase", () => ({ pb: { collection: () => ({ authWithPassword: mockAuthWithPassword, }), }, })); import LoginPage from "./page"; describe("LoginPage", () => { beforeEach(() => { vi.clearAllMocks(); }); describe("rendering", () => { it("renders the login form with email and password inputs", () => { render(); expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); expect(screen.getByLabelText(/password/i)).toBeInTheDocument(); }); it("renders a sign in button", () => { render(); expect( screen.getByRole("button", { name: /sign in/i }), ).toBeInTheDocument(); }); it("renders the PhaseFlow branding", () => { render(); expect(screen.getByText(/phaseflow/i)).toBeInTheDocument(); }); it("has email input with type email", () => { render(); const emailInput = screen.getByLabelText(/email/i); expect(emailInput).toHaveAttribute("type", "email"); }); it("has password input with type password", () => { render(); const passwordInput = screen.getByLabelText(/password/i); expect(passwordInput).toHaveAttribute("type", "password"); }); }); describe("form submission", () => { it("calls PocketBase auth with email and password on submit", async () => { mockAuthWithPassword.mockResolvedValueOnce({ token: "test-token" }); render(); const emailInput = screen.getByLabelText(/email/i); const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole("button", { name: /sign in/i }); fireEvent.change(emailInput, { target: { value: "test@example.com" } }); fireEvent.change(passwordInput, { target: { value: "password123" } }); fireEvent.click(submitButton); await waitFor(() => { expect(mockAuthWithPassword).toHaveBeenCalledWith( "test@example.com", "password123", ); }); }); it("redirects to dashboard on successful login", async () => { mockAuthWithPassword.mockResolvedValueOnce({ token: "test-token" }); render(); const emailInput = screen.getByLabelText(/email/i); const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole("button", { name: /sign in/i }); fireEvent.change(emailInput, { target: { value: "test@example.com" } }); fireEvent.change(passwordInput, { target: { value: "password123" } }); fireEvent.click(submitButton); await waitFor(() => { expect(mockPush).toHaveBeenCalledWith("/"); }); }); it("shows loading state while authenticating", async () => { // Create a promise that we can control let resolveAuth: (value: unknown) => void = () => {}; const authPromise = new Promise((resolve) => { resolveAuth = resolve; }); mockAuthWithPassword.mockReturnValue(authPromise); render(); const emailInput = screen.getByLabelText(/email/i); const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole("button", { name: /sign in/i }); fireEvent.change(emailInput, { target: { value: "test@example.com" } }); fireEvent.change(passwordInput, { target: { value: "password123" } }); fireEvent.click(submitButton); // Button should show loading state await waitFor(() => { expect( screen.getByRole("button", { name: /signing in/i }), ).toBeInTheDocument(); }); // Resolve the auth resolveAuth({ token: "test-token" }); }); it("disables form inputs while loading", async () => { let resolveAuth: (value: unknown) => void = () => {}; const authPromise = new Promise((resolve) => { resolveAuth = resolve; }); mockAuthWithPassword.mockReturnValue(authPromise); render(); const emailInput = screen.getByLabelText(/email/i); const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole("button", { name: /sign in/i }); fireEvent.change(emailInput, { target: { value: "test@example.com" } }); fireEvent.change(passwordInput, { target: { value: "password123" } }); fireEvent.click(submitButton); await waitFor(() => { expect(emailInput).toBeDisabled(); expect(passwordInput).toBeDisabled(); expect(screen.getByRole("button")).toBeDisabled(); }); resolveAuth({ token: "test-token" }); }); }); describe("error handling", () => { it("shows error message on failed login", async () => { mockAuthWithPassword.mockRejectedValueOnce( new Error("Invalid credentials"), ); render(); const emailInput = screen.getByLabelText(/email/i); const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole("button", { name: /sign in/i }); fireEvent.change(emailInput, { target: { value: "test@example.com" } }); fireEvent.change(passwordInput, { target: { value: "wrongpassword" } }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByRole("alert")).toBeInTheDocument(); expect(screen.getByText(/invalid credentials/i)).toBeInTheDocument(); }); }); it("clears error when user starts typing again", async () => { mockAuthWithPassword.mockRejectedValueOnce( new Error("Invalid credentials"), ); render(); const emailInput = screen.getByLabelText(/email/i); const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole("button", { name: /sign in/i }); fireEvent.change(emailInput, { target: { value: "test@example.com" } }); fireEvent.change(passwordInput, { target: { value: "wrongpassword" } }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByRole("alert")).toBeInTheDocument(); }); // Start typing again fireEvent.change(emailInput, { target: { value: "new@example.com" } }); expect(screen.queryByRole("alert")).not.toBeInTheDocument(); }); it("re-enables form after error", async () => { mockAuthWithPassword.mockRejectedValueOnce( new Error("Invalid credentials"), ); render(); const emailInput = screen.getByLabelText(/email/i); const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole("button", { name: /sign in/i }); fireEvent.change(emailInput, { target: { value: "test@example.com" } }); fireEvent.change(passwordInput, { target: { value: "wrongpassword" } }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByRole("alert")).toBeInTheDocument(); }); // Form should be re-enabled expect(emailInput).not.toBeDisabled(); expect(passwordInput).not.toBeDisabled(); expect( screen.getByRole("button", { name: /sign in/i }), ).not.toBeDisabled(); }); }); describe("validation", () => { it("does not submit with empty email", async () => { render(); const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole("button", { name: /sign in/i }); fireEvent.change(passwordInput, { target: { value: "password123" } }); fireEvent.click(submitButton); // Should not call auth expect(mockAuthWithPassword).not.toHaveBeenCalled(); }); it("does not submit with empty password", async () => { render(); const emailInput = screen.getByLabelText(/email/i); const submitButton = screen.getByRole("button", { name: /sign in/i }); fireEvent.change(emailInput, { target: { value: "test@example.com" } }); fireEvent.click(submitButton); // Should not call auth expect(mockAuthWithPassword).not.toHaveBeenCalled(); }); }); });