// @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();
});
});