// @vitest-environment jsdom import { render, screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, expectTypeOf, it } from "vitest"; import { z } from "zod"; import { createMemoryRouter, matchRoutes, Outlet, RouterProvider, } from "react-router-dom"; import { createAnonymousSessionAdapter } from "../../src/adapters/auth/external-session-adapter.ts"; import { createTechLogFeatureInstalledInput } from "../../src/features/tech-log/adapters/create-tech-log-feature-input.ts"; import { TECH_LOG_FEATURE_ID } from "../../src/features/tech-log/application/tech-log-feature-input.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_CODECS, 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["*"])}

; } const reviewFixtureCodecs = Object.freeze({ ReviewFixtureParams: z .object({ reviewId: z.string().min(1) }) .strict(), ReviewFixtureSearch: z .object({ filter: z.preprocess( (value) => (Array.isArray(value) ? value[0] : value), z.string().trim().min(1).optional(), ), }) .strip(), }); const groupedFixtureCodecs = Object.freeze({ none: ROUTE_CODECS.none, NotFoundSplat: ROUTE_CODECS.NotFoundSplat, }); const reviewFixtureRegistry = Object.freeze({ REVIEW_FIXTURE: Object.freeze({ ...ROUTE_REGISTRY.TECH_LOG_HOME, routeId: "REVIEW_FIXTURE", path: "/review/:reviewId", paramsSchema: "ReviewFixtureParams", searchSchema: "ReviewFixtureSearch", }), }); function ReviewRouteFixture() { const input = useRouteInput<"REVIEW_FIXTURE">(); expectTypeOf(input.routeId).toEqualTypeOf<"REVIEW_FIXTURE">(); return (

{input.routeId}

{String(input.params.reviewId)}

{JSON.stringify(input.search)}

); } const reviewFixtureRuntime = Object.freeze({ REVIEW_FIXTURE: Object.freeze({ moduleId: "review-fixture", Component: ReviewRouteFixture, }), }); function compileTimeGroupedRouteContract() { // @ts-expect-error A grouped route composition must provide its codec registry. createGroupedRouteObjects( reviewFixtureRegistry, reviewFixtureRuntime, { PUBLIC: , STUDIO: }, "type-test-build", ); return createGroupedRouteObjects( reviewFixtureRegistry, reviewFixtureRuntime, { PUBLIC: , STUDIO: }, "type-test-build", reviewFixtureCodecs, ); } void compileTimeGroupedRouteContract; function renderRouter() { const techLog = createTechLogFeatureInstalledInput(); return render( , ); } describe("generic application router", () => { it("renders and canonicalizes an isolated non-installed route codec contract", async () => { expect(ROUTE_REGISTRY).not.toHaveProperty("REVIEW_FIXTURE"); expect(ROUTE_CODECS).not.toHaveProperty("ReviewFixtureParams"); expect(ROUTE_CODECS).not.toHaveProperty("ReviewFixtureSearch"); const routes = createGroupedRouteObjects( reviewFixtureRegistry, reviewFixtureRuntime, { PUBLIC: , STUDIO: }, "test-build", reviewFixtureCodecs, ); const router = createMemoryRouter(routes, { initialEntries: [ "/review/non-empty?filter=%20first%20&filter=second&unknown=drop", ], }); render( , ); expect( await screen.findByRole("heading", { name: "REVIEW_FIXTURE" }), ).toBeVisible(); expect(screen.getByTestId("review-param")).toHaveTextContent("non-empty"); expect(screen.getByTestId("review-search")).toHaveTextContent( '{"filter":"first"}', ); await waitFor(() => expect(router.state.location).toMatchObject({ pathname: "/review/non-empty", search: "?filter=first", }), ); }); it("assembles generic Public and Studio parents with Studio catch-all precedence", () => { const registry = { TECH_LOG_HOME: ROUTE_REGISTRY.TECH_LOG_HOME, STUDIO_FIXTURE: { ...ROUTE_REGISTRY.NOT_FOUND, routeId: "STUDIO_FIXTURE", path: "/studio/*", layoutGroup: "STUDIO" as const, }, NOT_FOUND: ROUTE_REGISTRY.NOT_FOUND, }; const runtime = { TECH_LOG_HOME: ROUTE_RUNTIME.TECH_LOG_HOME, STUDIO_FIXTURE: ROUTE_RUNTIME.NOT_FOUND, NOT_FOUND: ROUTE_RUNTIME.NOT_FOUND, }; const routes = createGroupedRouteObjects( registry, runtime, { PUBLIC:
, STUDIO:
, }, "test-build", groupedFixtureCodecs, ); 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", groupedFixtureCodecs, ); 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", groupedFixtureCodecs, ), ).toThrow("Missing route runtime: TECH_LOG_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", groupedFixtureCodecs, ); 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.TECH_LOG_HOME, routeId: "PROTECTED_FIXTURE", path: "/private-fixture", access: "session-required", searchSchema: null, }, }, { PROTECTED_FIXTURE: { moduleId: "protected-fixture", Component: StudioFallbackFixture, }, }, { PUBLIC: , STUDIO: , }, "test-build", groupedFixtureCodecs, ); const router = createMemoryRouter(routes, { initialEntries: ["/private-fixture"], }); render( , ); expect( await screen.findByRole("heading", { name: "세션이 필요합니다." }), ).toBeVisible(); }); it("renders the installed TechLog home in the Public layout", async () => { const user = userEvent.setup(); window.history.pushState({}, "", "/"); renderRouter(); expect( await screen.findByRole("heading", { name: "TechLog", level: 1 }), ).toBeVisible(); expect(screen.getByRole("navigation", { name: "주요 탐색" })).toBeVisible(); await user.click( within(screen.getByRole("navigation", { name: "주요 탐색" })).getByRole( "link", { name: "프로젝트" }, ), ); expect( await screen.findByRole("heading", { name: "프로젝트", level: 1 }), ).toBeVisible(); expect(window.location.pathname).toBe("/projects"); }); it("keeps the unreachable client-side Public fallback inside its accessible shell", async () => { window.history.pushState({}, "", "/missing"); renderRouter(); expect( await screen.findByRole("heading", { name: "페이지를 찾을 수 없습니다." }), ).toBeVisible(); expect(screen.getByRole("navigation", { name: "주요 탐색" })).toBeVisible(); expect(screen.queryByText("Not Found", { exact: true })).not.toBeInTheDocument(); }); it("keeps Studio routes inside the persistent Studio layout", async () => { window.history.pushState({}, "", "/studio"); renderRouter(); expect( await screen.findByRole("heading", { name: "작업 흐름" }), ).toBeVisible(); expect(screen.getByRole("navigation", { name: "Studio 주 탐색" })).toBeVisible(); expect(screen.getByRole("link", { name: "공개 사이트 보기" })).toHaveAttribute( "href", "/", ); }); it("gives the Studio wildcard precedence over the Public not-found route", async () => { window.history.pushState({}, "", "/studio/missing"); renderRouter(); expect( await screen.findByRole("heading", { name: "Studio 화면을 찾을 수 없습니다", }), ).toBeVisible(); expect(screen.getByRole("navigation", { name: "Studio 주 탐색" })).toBeVisible(); expect(window.location.pathname).toBe("/studio/missing"); }); });