// @vitest-environment jsdom import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it } from "vitest"; import { createMemoryRouter, matchRoutes, Outlet, RouterProvider, } from "react-router-dom"; import { createAnonymousSessionAdapter } from "../../src/adapters/auth/external-session-adapter.ts"; import { ApplicationProvider } from "../../src/presentation/providers/application-provider.tsx"; import { AppRouter, createGroupedRouteObjects, } from "../../src/presentation/routes/app-router.tsx"; import { createTestApplication } from "../helpers/create-test-application.ts"; import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.ts"; import { ROUTE_RUNTIME } from "../../src/features/installed-feature-runtimes.tsx"; import { LocaleProvider } from "../../src/presentation/i18n/index.ts"; import { SessionProvider } from "../../src/presentation/providers/session-provider.tsx"; import { ThemeProvider } from "../../src/presentation/providers/theme-provider.tsx"; import { useRouteInput } from "../../src/presentation/routes/route-input.tsx"; function StudioFallbackFixture() { const input = useRouteInput(); return

{String(input.params["*"])}

; } function renderRouter() { return render( , ); } describe("generic application router", () => { it("assembles generic Public and Studio parents with Studio catch-all precedence", () => { const registry = { APP_HOME: ROUTE_REGISTRY.APP_HOME, STUDIO_FIXTURE: { ...ROUTE_REGISTRY.NOT_FOUND, routeId: "STUDIO_FIXTURE", path: "/studio/*", layoutGroup: "STUDIO" as const, }, NOT_FOUND: ROUTE_REGISTRY.NOT_FOUND, }; const runtime = { APP_HOME: ROUTE_RUNTIME.APP_HOME, STUDIO_FIXTURE: ROUTE_RUNTIME.NOT_FOUND, NOT_FOUND: ROUTE_RUNTIME.NOT_FOUND, }; const routes = createGroupedRouteObjects( registry, runtime, { PUBLIC:
, STUDIO:
, }, "test-build", ); expect(routes.map((route) => route.id)).toEqual([ "STUDIO_LAYOUT", "PUBLIC_LAYOUT", ]); expect( matchRoutes(routes, "/studio/unknown")?.map((match) => match.route.id), ).toEqual(["STUDIO_LAYOUT", "STUDIO_FIXTURE"]); expect( matchRoutes(routes, "/publicly-unknown")?.map((match) => match.route.id), ).toEqual(["PUBLIC_LAYOUT", "NOT_FOUND"]); const installedRoutes = createGroupedRouteObjects( ROUTE_REGISTRY, ROUTE_RUNTIME, { PUBLIC:
, STUDIO:
, }, "test-build", ); expect(installedRoutes[1]?.children?.at(-1)?.id).toBe("NOT_FOUND"); }); it("rejects a grouped registry whose leaf runtime is missing", () => { expect(() => createGroupedRouteObjects( ROUTE_REGISTRY, {}, { PUBLIC:
, STUDIO:
, }, "test-build", ), ).toThrow("Missing route runtime: APP_HOME"); }); it("renders a non-installed Studio wildcard leaf without rewriting its URL", async () => { const routes = createGroupedRouteObjects( { STUDIO_FIXTURE: { ...ROUTE_REGISTRY.NOT_FOUND, routeId: "STUDIO_FIXTURE", path: "/studio/*", layoutGroup: "STUDIO", }, }, { STUDIO_FIXTURE: { moduleId: "studio-fixture", Component: StudioFallbackFixture, }, }, { PUBLIC: , STUDIO: (
), }, "test-build", ); const router = createMemoryRouter(routes, { initialEntries: ["/studio/unknown/path"], }); render( , ); expect(await screen.findByTestId("studio-layout")).toBeVisible(); expect( await screen.findByRole("heading", { name: "unknown/path" }), ).toBeVisible(); expect(router.state.location.pathname).toBe("/studio/unknown/path"); }); it("applies authorization from a non-installed route definition", async () => { const routes = createGroupedRouteObjects( { PROTECTED_FIXTURE: { ...ROUTE_REGISTRY.APP_HOME, routeId: "PROTECTED_FIXTURE", path: "/private-fixture", access: "session-required", }, }, { PROTECTED_FIXTURE: { moduleId: "protected-fixture", Component: StudioFallbackFixture, }, }, { PUBLIC: , STUDIO: , }, "test-build", ); const router = createMemoryRouter(routes, { initialEntries: ["/private-fixture"], }); render( , ); expect( await screen.findByRole("heading", { name: "세션이 필요합니다." }), ).toBeVisible(); }); it("reaches the platform overview from the home starter actions", async () => { const user = userEvent.setup(); window.history.pushState({}, "", "/"); renderRouter(); // The starter action lives inside the lazily loaded home chunk, so this is // the first wait in the file that has to outlast a chunk load rather than // an already-mounted shell element. await user.click( await screen.findByRole( "link", { name: "플랫폼 구성 보기" }, { timeout: 5000 }, ), ); await waitFor(() => expect( screen.getByRole("heading", { name: "플랫폼 구성", level: 1 }), ).toHaveFocus(), ); }); it("renders the app shell and not-found route without a feature input", async () => { window.history.pushState({}, "", "/missing"); renderRouter(); expect( await screen.findByRole("heading", { name: "페이지를 찾을 수 없습니다.", }), ).toBeVisible(); expect(screen.getByRole("navigation", { name: "주요 탐색" })).toBeVisible(); expect(screen.getByRole("main")).toBeVisible(); }); it("preserves authorization around grouped registered leaves", async () => { window.history.pushState({}, "", "/examples/reference-resources"); renderRouter(); expect( await screen.findByRole("heading", { name: "세션이 필요합니다." }), ).toBeVisible(); expect(screen.getByRole("navigation", { name: "주요 탐색" })).toBeVisible(); }); it("navigates between registry-backed platform routes", async () => { const user = userEvent.setup(); window.history.pushState({}, "", "/"); renderRouter(); await user.click( await screen.findByRole("link", { name: "UI 구성요소" }), ); expect( await screen.findByRole("heading", { name: "UI 구성요소", level: 1 }), ).toBeVisible(); expect(window.location.pathname).toBe("/examples/ui"); expect(document.title).toBe("UI 구성요소 · Tech Log"); await waitFor(() => expect( screen.getByRole("heading", { name: "UI 구성요소", level: 1 }), ).toHaveFocus(), ); }); it("focuses the route heading again when the lazy chunk is already cached", async () => { // §9.7. The first visit resolves the route module asynchronously, so the // router lifecycle and the page header commit separately. A later visit // renders the cached module in the same commit, which is the ordering that // must still hand focus to the heading rather than leaving it on main. const user = userEvent.setup(); window.history.pushState({}, "", "/"); renderRouter(); for (const label of ["UI 구성요소", "화면 상태", "UI 구성요소"]) { await user.click(await screen.findByRole("link", { name: label })); await waitFor(() => expect( screen.getByRole("heading", { name: label, level: 1 }), ).toHaveFocus(), ); } }); });