import { render, screen, fireEvent, act } from "@testing-library/react";
/* eslint-disable react-x/component-hook-factories --
The vi.mock factory must expose a hook-named export (useAuth) to stand in
for the real module; it's a test double, not a React hook. */
import { describe, it, expect, beforeEach, vi } from "vitest";
import { AuthGuard } from "./auth-guard";
interface AuthState {
isReady: boolean;
isAuthenticated: boolean;
}
// Shared, mutable test doubles. Declared via vi.hoisted so they can be
// referenced inside the (hoisted) vi.mock factory below.
const h = vi.hoisted(() => {
const state: { isReady: boolean; isAuthenticated: boolean } = {
isReady: false,
isAuthenticated: false,
};
return {
login: vi.fn(),
logout: vi.fn(),
state,
};
});
vi.mock("@tailor-platform/app-shell", () => ({
useAuth: () => ({
login: h.login,
logout: h.logout,
isReady: h.state.isReady,
isAuthenticated: h.state.isAuthenticated,
}),
}));
// jsdom's navigator.onLine defaults to `true` and is writable, but we toggle
// it via Object.defineProperty so it survives across tests and so the
// snapshot read inside useSyncExternalStore picks up the change.
function setOnline(value: boolean) {
Object.defineProperty(navigator, "onLine", {
configurable: true,
get: () => value,
});
}
beforeEach(() => {
h.login.mockClear();
h.logout.mockClear();
h.state = { isReady: false, isAuthenticated: false };
setOnline(true);
});
const renderGuard = () => render();
const setAuth = (next: Partial) => {
h.state = { ...h.state, ...next };
};
describe("AuthGuard", () => {
it("shows a loading spinner while the auth bootstrap is in flight", () => {
setAuth({ isReady: false, isAuthenticated: false });
renderGuard();
expect(screen.getByRole("status")).toBeInTheDocument();
expect(screen.getByText("Loading…")).toBeInTheDocument();
expect(screen.queryByText("Sign in required")).not.toBeInTheDocument();
});
it("shows the connectivity-error screen (not 'Sign in required') when the browser is offline", () => {
// The case the user reported: disconnect WiFi -> app-shell can't reach the
// IdP -> reports isAuthenticated=false. We must NOT mislead the user into
// clicking Sign in; show a connectivity error instead.
setOnline(false);
setAuth({ isReady: true, isAuthenticated: false });
renderGuard();
expect(screen.getByText("Can't reach the server")).toBeInTheDocument();
expect(screen.queryByText("Sign in required")).not.toBeInTheDocument();
// Connectivity screen rendered without an onRetry, so no retry button.
expect(screen.queryByRole("button", { name: "Retry" })).not.toBeInTheDocument();
expect(h.login).not.toHaveBeenCalled();
});
it("does not mention the VPN/exit-node anywhere on the connectivity screen", () => {
// Prod is publicly reachable — VPN copy would be misleading there.
setOnline(false);
setAuth({ isReady: true, isAuthenticated: false });
renderGuard();
expect(screen.queryByText(/VPN/i)).not.toBeInTheDocument();
expect(screen.queryByText(/exit node/i)).not.toBeInTheDocument();
});
it("auto-recovers when the browser comes back online", () => {
setOnline(false);
setAuth({ isReady: true, isAuthenticated: false });
renderGuard();
expect(screen.getByText("Can't reach the server")).toBeInTheDocument();
// Browser regains connectivity -> hook re-renders -> guard falls through
// to the sign-in branch on its own. No manual refresh required.
act(() => {
setOnline(true);
window.dispatchEvent(new Event("online"));
});
expect(screen.getByText("Sign in required")).toBeInTheDocument();
expect(screen.queryByText("Can't reach the server")).not.toBeInTheDocument();
});
it("shows 'Sign in required' when online and not authenticated", () => {
setAuth({ isReady: true, isAuthenticated: false });
renderGuard();
expect(screen.getByText("Sign in required")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Sign in" }));
expect(h.login).toHaveBeenCalledTimes(1);
});
it("renders nothing (falls through) once the user is authenticated", () => {
setAuth({ isReady: true, isAuthenticated: true });
const { container } = renderGuard();
// AuthProvider treats an empty/undefined guard return as "guard passes"
// and renders its own children; here we just assert no guard UI shows.
expect(container).toBeEmptyDOMElement();
expect(screen.queryByText("Sign in required")).not.toBeInTheDocument();
expect(screen.queryByText("Can't reach the server")).not.toBeInTheDocument();
});
});