/**
* @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 });
});
});
});