// @vitest-environment jsdom import { render, screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { Outlet, RouterProvider, createMemoryRouter } from "react-router-dom"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createTechLogFeatureInstalledInput } from "../../../src/features/tech-log/adapters/create-tech-log-feature-input.ts"; import { MOCK_STUDIO_INSTALL_CONTEXT } from "../../helpers/studio-install-context.ts"; import type { StudioGateway } from "../../../src/features/tech-log/application/ports/studio-gateway.ts"; import { StudioHomePage } from "../../../src/features/tech-log/presentation/studio/pages/studio-home-page.tsx"; import { StudioNotFoundPage } from "../../../src/features/tech-log/presentation/studio/pages/studio-not-found-page.tsx"; import { StudioShell } from "../../../src/features/tech-log/presentation/studio/studio-shell.tsx"; import { createExternalAuthSessionAdapter } from "../../../src/adapters/auth/external-session-adapter.ts"; import { ApplicationProvider } from "../../../src/presentation/providers/application-provider.tsx"; import { SessionProvider } from "../../../src/presentation/providers/session-provider.tsx"; import { createTestApplication } from "../../helpers/create-test-application.ts"; import { renderWithQueryProviders } from "../../helpers/query-providers.tsx"; afterEach(() => vi.restoreAllMocks()); async function renderStudio( initialEntry: string, createStudioGateway: () => StudioGateway, ) { const router = createMemoryRouter( [ { path: "/studio", element: ( ), children: [ { index: true, element: }, { path: "documents", element:

작업본 라우트

}, { path: "documents/new", element:

새 문서 라우트

}, { path: "publications", element:

게시 기록 라우트

}, { path: "*", element: }, ], }, ], { initialEntries: [initialEntry] }, ); const installed = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input; const application = createTestApplication({ // The Studio shell draws its chrome only for an authenticated session — a // signed-out visitor gets the sign-in surface and nothing else, which is // what these tests are not about. In the real app AppRouter supplies both // the session state and the provider; here the harness does. session: createExternalAuthSessionAdapter({ readState: () => "authenticated" as const, subscribe: () => () => {}, beginSignIn: async () => {}, signOut: async () => {}, attachCredential: async () => ({ headers: {} }), recoverSession: async () => "restored" as const, notifyUnauthenticated: () => {}, }), featureInputs: { "tech-log": { ...installed, createStudioGateway }, }, }); const view = render( renderWithQueryProviders( , )); // 포트가 async 가 되면서 첫 페인트에는 데이터가 없다. 화면이 정착한 뒤 // 단언하도록 여기서 한 번 기다린다 — 각 테스트에 흩어 놓으면 빠뜨린 곳이 생긴다. await screen.findByRole("main"); return { ...view, router }; } describe("TechLog Studio shell", () => { it("creates one application-provided gateway for child navigation and exposes the source header", async () => { const user = userEvent.setup(); const gateway = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); const createGateway = vi.fn(() => gateway); const { container, router } = await renderStudio("/studio", createGateway); expect(await screen.findByRole("heading", { level: 1, name: "작업 흐름" })).toBeVisible(); expect(createGateway).toHaveBeenCalledTimes(1); expect( Array.from( container.querySelectorAll(".studio-desktop-navigation a"), (link) => [link.textContent, link.getAttribute("href")], ), ).toEqual([ ["작업본", "/studio/documents"], ["게시 기록", "/studio/publications"], ["새 문서", "/studio/documents/new"], ["주제·프로젝트", "/studio/taxonomy"], ["공개 사이트 보기", "/"], ]); expect(screen.getByRole("link", { name: "공개 사이트 보기" })).toHaveAttribute("href", "/"); expect(screen.queryByText(/로그인|sign in/i)).not.toBeInTheDocument(); await user.click(within(screen.getByRole("navigation", { name: "Studio 주 탐색" })).getByRole("link", { name: "작업본" })); expect(router.state.location.pathname).toBe("/studio/documents"); expect(screen.getByText("작업본 라우트")).toBeVisible(); expect(createGateway).toHaveBeenCalledTimes(1); expect(screen.getAllByRole("link", { name: "작업본" })[0]).toHaveAttribute("aria-current", "page"); }); it("recreates the provider generation and cancels obsolete work after a persisted pageshow", async () => { const user = userEvent.setup(); let firstSignal: AbortSignal | undefined; const first = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); const second = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); vi.spyOn(first, "getDashboard").mockImplementation(({ signal } = {}) => { firstSignal = signal; return new Promise(() => {}); }); const secondDashboard = vi.spyOn(second, "getDashboard"); const createGateway = vi.fn() .mockReturnValueOnce(first) .mockReturnValueOnce(second); await renderStudio("/studio", createGateway); await waitFor(() => expect(firstSignal).toBeDefined()); await user.click(screen.getByRole("button", { name: "Studio 메뉴 열기" })); expect(screen.getByRole("button", { name: "Studio 메뉴 닫기" })).toBeVisible(); window.dispatchEvent(new PageTransitionEvent("pageshow", { persisted: true })); await waitFor(() => expect(createGateway).toHaveBeenCalledTimes(2)); expect(firstSignal?.aborted).toBe(true); expect(screen.getByRole("button", { name: "Studio 메뉴 열기" })).toHaveAttribute("aria-expanded", "false"); expect(await screen.findByRole("heading", { level: 1, name: "작업 흐름" })).toBeVisible(); expect(secondDashboard).toHaveBeenCalledTimes(1); }); it("keeps unknown Studio routes inside the Studio shell without authentication UI", async () => { const createGateway = vi.fn( () => createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(), ); await renderStudio("/studio/does-not-exist", createGateway); expect(screen.getByRole("banner")).toHaveClass("studio-header"); expect(screen.getByRole("heading", { level: 1, name: "Studio 화면을 찾을 수 없습니다" })).toBeVisible(); expect(screen.getByRole("link", { name: "작업본으로 돌아가기" })).toHaveAttribute("href", "/studio/documents"); expect(screen.queryByText(/로그인|sign in/i)).not.toBeInTheDocument(); }); });