/** * @vitest-environment jsdom */ import { cleanup, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { Router } from "wouter"; import { memoryLocation } from "wouter/memory-location"; import { apiClient } from "../api/client"; import { AuthProvider } from "../stores"; import { LoginPage } from "./LoginPage"; vi.mock("../api/client", () => ({ apiClient: { login: vi.fn(), logout: vi.fn(), isAuthenticated: vi.fn(), getTokens: vi.fn(), }, ApiClientError: class ApiClientError extends Error { constructor( message: string, public status: number, public code?: string, ) { super(message); this.name = "ApiClientError"; } }, })); function renderWithProviders(path = "/login") { const { hook } = memoryLocation({ path }); return render( , ); } describe("LoginPage", () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(apiClient.getTokens).mockReturnValue(null); vi.mocked(apiClient.isAuthenticated).mockReturnValue(false); }); afterEach(() => { cleanup(); vi.restoreAllMocks(); }); it("renders login form", async () => { renderWithProviders(); expect(screen.getByRole("heading", { name: "Kioku" })).toBeDefined(); expect(screen.getByRole("heading", { name: "Welcome back" })).toBeDefined(); expect(screen.getByLabelText("Username")).toBeDefined(); expect(screen.getByLabelText("Password")).toBeDefined(); expect(screen.getByRole("button", { name: "Sign in" })).toBeDefined(); }); it("submits form and logs in successfully", async () => { const user = userEvent.setup(); const mockUser = { id: "user-1", username: "testuser" }; vi.mocked(apiClient.login).mockResolvedValue({ accessToken: "access-token", refreshToken: "refresh-token", user: mockUser, }); renderWithProviders(); await user.type(screen.getByLabelText("Username"), "testuser"); await user.type(screen.getByLabelText("Password"), "password123"); await user.click(screen.getByRole("button", { name: "Sign in" })); await waitFor(() => { expect(apiClient.login).toHaveBeenCalledWith("testuser", "password123"); }); }); it("displays error on login failure", async () => { const user = userEvent.setup(); const { ApiClientError } = await import("../api/client"); vi.mocked(apiClient.login).mockRejectedValue( new ApiClientError("Invalid credentials", 401), ); renderWithProviders(); await user.type(screen.getByLabelText("Username"), "testuser"); await user.type(screen.getByLabelText("Password"), "wrongpassword"); await user.click(screen.getByRole("button", { name: "Sign in" })); await waitFor(() => { expect(screen.getByRole("alert").textContent).toBe("Invalid credentials"); }); }); it("displays generic error on unexpected failure", async () => { const user = userEvent.setup(); vi.mocked(apiClient.login).mockRejectedValue(new Error("Network error")); renderWithProviders(); await user.type(screen.getByLabelText("Username"), "testuser"); await user.type(screen.getByLabelText("Password"), "password123"); await user.click(screen.getByRole("button", { name: "Sign in" })); await waitFor(() => { expect(screen.getByRole("alert").textContent).toBe( "Login failed. Please try again.", ); }); }); it("disables form while submitting", async () => { const user = userEvent.setup(); vi.mocked(apiClient.login).mockImplementation( () => new Promise(() => {}), // Never resolves ); renderWithProviders(); await user.type(screen.getByLabelText("Username"), "testuser"); await user.type(screen.getByLabelText("Password"), "password123"); await user.click(screen.getByRole("button", { name: "Sign in" })); await waitFor(() => { const button = screen.getByRole("button", { name: /Signing in/ }); expect(button.hasAttribute("disabled")).toBe(true); }); expect( (screen.getByLabelText("Username") as HTMLInputElement).disabled, ).toBe(true); expect( (screen.getByLabelText("Password") as HTMLInputElement).disabled, ).toBe(true); }); it("redirects when already authenticated", async () => { vi.mocked(apiClient.isAuthenticated).mockReturnValue(true); vi.mocked(apiClient.getTokens).mockReturnValue({ accessToken: "access-token", refreshToken: "refresh-token", }); const { hook } = memoryLocation({ path: "/login" }); const navigateSpy = vi.fn(); const hookWithSpy: typeof hook = () => { const result = hook(); return [result[0], navigateSpy]; }; render( , ); await waitFor(() => { expect(navigateSpy).toHaveBeenCalledWith("/", { replace: true }); }); }); });