| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191 |
- import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
- import { createRoot } from "react-dom/client";
- import { act } from "react";
- import LoginPage from "@/app/[locale]/login/page";
- const mockPush = vi.hoisted(() => vi.fn());
- const mockRefresh = vi.hoisted(() => vi.fn());
- const mockUseRouter = vi.hoisted(() => vi.fn(() => ({ push: mockPush, refresh: mockRefresh })));
- const mockUseSearchParams = vi.hoisted(() => vi.fn(() => ({ get: vi.fn(() => null) })));
- const mockUseTranslations = vi.hoisted(() => vi.fn(() => (key: string) => `t:${key}`));
- const mockUseLocale = vi.hoisted(() => vi.fn(() => "en"));
- const mockUsePathname = vi.hoisted(() => vi.fn(() => "/login"));
- vi.mock("next/navigation", () => ({
- useSearchParams: mockUseSearchParams,
- useRouter: mockUseRouter,
- usePathname: mockUsePathname,
- }));
- vi.mock("next-intl", () => ({
- useTranslations: mockUseTranslations,
- useLocale: mockUseLocale,
- }));
- vi.mock("@/i18n/routing", () => ({
- Link: ({ children, ...props }: any) => <a {...props}>{children}</a>,
- useRouter: mockUseRouter,
- usePathname: mockUsePathname,
- }));
- vi.mock("next-themes", () => ({
- useTheme: vi.fn(() => ({ theme: "system", setTheme: vi.fn() })),
- }));
- const globalFetch = global.fetch;
- describe("LoginPage Loading State", () => {
- let container: HTMLDivElement;
- let root: ReturnType<typeof createRoot>;
- beforeEach(() => {
- container = document.createElement("div");
- document.body.appendChild(container);
- root = createRoot(container);
- vi.clearAllMocks();
- global.fetch = vi.fn().mockResolvedValue({
- ok: true,
- json: async () => ({}),
- });
- });
- afterEach(() => {
- act(() => {
- root.unmount();
- });
- document.body.removeChild(container);
- global.fetch = globalFetch;
- });
- const render = async () => {
- await act(async () => {
- root.render(<LoginPage />);
- });
- };
- const setInputValue = (input: HTMLInputElement, value: string) => {
- const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
- window.HTMLInputElement.prototype,
- "value"
- )?.set;
- if (nativeInputValueSetter) {
- nativeInputValueSetter.call(input, value);
- } else {
- input.value = value;
- }
- input.dispatchEvent(new Event("input", { bubbles: true }));
- };
- const getSubmitButton = () =>
- container.querySelector('button[type="submit"]') as HTMLButtonElement;
- const getApiKeyInput = () => container.querySelector("input#apiKey") as HTMLInputElement;
- const getOverlay = () => container.querySelector('[data-testid="loading-overlay"]');
- it("starts in idle state with no overlay", async () => {
- await render();
- expect(getOverlay()).toBeNull();
- expect(getSubmitButton().disabled).toBe(true);
- expect(getApiKeyInput().disabled).toBe(false);
- });
- it("shows fullscreen overlay during submission", async () => {
- let resolveFetch: (value: any) => void;
- const fetchPromise = new Promise((resolve) => {
- resolveFetch = resolve;
- });
- (global.fetch as any).mockReturnValue(fetchPromise);
- await render();
- const input = getApiKeyInput();
- await act(async () => {
- setInputValue(input, "test-api-key");
- });
- const button = getSubmitButton();
- await act(async () => {
- button.click();
- });
- const overlay = getOverlay();
- expect(overlay).not.toBeNull();
- expect(overlay?.textContent).toContain("t:login.loggingIn");
- expect(getSubmitButton().disabled).toBe(true);
- expect(getApiKeyInput().disabled).toBe(true);
- await act(async () => {
- resolveFetch!({
- ok: true,
- json: async () => ({ redirectTo: "/dashboard" }),
- });
- });
- });
- it("keeps overlay on success until redirect", async () => {
- (global.fetch as any).mockResolvedValue({
- ok: true,
- json: async () => ({ redirectTo: "/dashboard" }),
- });
- await render();
- const input = getApiKeyInput();
- await act(async () => {
- setInputValue(input, "test-api-key");
- });
- await act(async () => {
- getSubmitButton().click();
- });
- const overlay = getOverlay();
- expect(overlay).not.toBeNull();
- expect(mockPush).toHaveBeenCalledWith("/dashboard");
- expect(mockRefresh).toHaveBeenCalled();
- });
- it("removes overlay and shows error on failure", async () => {
- (global.fetch as any).mockResolvedValue({
- ok: false,
- json: async () => ({ error: "Invalid key" }),
- });
- await render();
- const input = getApiKeyInput();
- await act(async () => {
- setInputValue(input, "test-api-key");
- });
- await act(async () => {
- getSubmitButton().click();
- });
- expect(getOverlay()).toBeNull();
- expect(container.textContent).toContain("Invalid key");
- expect(getSubmitButton().disabled).toBe(false);
- expect(getApiKeyInput().disabled).toBe(false);
- });
- it("removes overlay and shows error on network exception", async () => {
- (global.fetch as any).mockRejectedValue(new Error("Network error"));
- await render();
- const input = getApiKeyInput();
- await act(async () => {
- setInputValue(input, "test-api-key");
- });
- await act(async () => {
- getSubmitButton().click();
- });
- expect(getOverlay()).toBeNull();
- expect(container.textContent).toContain("t:errors.networkError");
- expect(getSubmitButton().disabled).toBe(false);
- });
- });
|