diff --git a/frontend/src/Router.test.tsx b/frontend/src/Router.test.tsx
new file mode 100644
index 000000000..bd6114807
--- /dev/null
+++ b/frontend/src/Router.test.tsx
@@ -0,0 +1,79 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup, render, screen, waitFor } from "@testing-library/react";
+import type { ReactNode } from "react";
+import Router from "src/Router";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+const { authState } = vi.hoisted(() => ({ authState: { authenticated: true } }));
+
+vi.mock("src/context", () => ({ useAuthState: () => authState }));
+vi.mock("src/hooks", () => ({
+ useHealth: () => ({ data: { status: "OK", setup: true }, isLoading: false, isError: false }),
+}));
+vi.mock("src/components", () => ({
+ Page: ({ children }: { children: ReactNode }) => children,
+ SiteContainer: ({ children }: { children: ReactNode }) => children,
+ SiteHeader: () => null,
+ SiteMenu: () => null,
+ SiteFooter: () => null,
+ LoadingPage: () =>
Loading
,
+ Unhealthy: () => Unhealthy
,
+ ErrorNotFound: () => Not found
,
+}));
+vi.mock("src/pages/Dashboard", () => ({ default: () => Dashboard
}));
+vi.mock("src/pages/Login", () => ({ default: () => Login
}));
+vi.mock("src/pages/Nginx/ProxyHosts", () => ({ default: () => Proxy hosts
}));
+
+describe("Router", () => {
+ beforeEach(() => {
+ authState.authenticated = true;
+ window.history.replaceState(null, "", "/");
+ });
+
+ afterEach(() => {
+ cleanup();
+ vi.restoreAllMocks();
+ });
+
+ it.each(["/login", "/login/", "/login?next=/users#form"])(
+ "redirects an authenticated visit to %s to the dashboard",
+ async (path) => {
+ window.history.replaceState(null, "", path);
+ const replaceState = vi.spyOn(window.history, "replaceState");
+ render();
+
+ expect(await screen.findByRole("heading", { name: "Dashboard" })).toBeVisible();
+ expect(window.location.pathname).toBe("/");
+ expect(window.location.search).toBe("");
+ expect(window.location.hash).toBe("");
+ expect(replaceState).toHaveBeenCalledWith(expect.anything(), "", "/");
+ },
+ );
+
+ it("shows the login form when signed out and redirects after sign-in", async () => {
+ authState.authenticated = false;
+ window.history.replaceState(null, "", "/login");
+ const { rerender } = render();
+
+ expect(await screen.findByRole("heading", { name: "Login" })).toBeVisible();
+ expect(window.location.pathname).toBe("/login");
+
+ authState.authenticated = true;
+ rerender();
+
+ expect(await screen.findByRole("heading", { name: "Dashboard" })).toBeVisible();
+ await waitFor(() => expect(window.location.pathname).toBe("/"));
+ });
+
+ it.each([
+ ["/", "Dashboard"],
+ ["/nginx/proxy", "Proxy hosts"],
+ ["/unknown", "Not found"],
+ ])("preserves the existing route for %s", async (path, heading) => {
+ window.history.replaceState(null, "", path);
+ render();
+
+ expect(await screen.findByRole("heading", { name: heading })).toBeVisible();
+ expect(window.location.pathname).toBe(path);
+ });
+});
diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx
index 6aa8f0894..d0ee7b18b 100644
--- a/frontend/src/Router.tsx
+++ b/frontend/src/Router.tsx
@@ -1,5 +1,5 @@
import { lazy, Suspense } from "react";
-import { BrowserRouter, Route, Routes } from "react-router-dom";
+import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
import {
ErrorNotFound,
LoadingPage,
@@ -61,6 +61,7 @@ function Router() {
}>
} />
+ } />
} />
} />
} />