// @vitest-environment jsdom import { render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createMemoryRouter, RouterProvider } from "react-router-dom"; 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 { FatalErrorState } from "../../../src/features/tech-log/presentation/public/components/fatal-error-state.tsx"; import { PublicShell } from "../../../src/features/tech-log/presentation/public/public-shell.tsx"; import { ApplicationProvider } from "../../../src/presentation/providers/application-provider.tsx"; import { createTestApplication } from "../../helpers/create-test-application.ts"; import { renderWithQueryProviders } from "../../helpers/query-providers.tsx"; const originalShowModal = HTMLDialogElement.prototype.showModal; const originalClose = HTMLDialogElement.prototype.close; beforeEach(() => { Object.defineProperty(HTMLDialogElement.prototype, "showModal", { configurable: true, value(this: HTMLDialogElement) { this.setAttribute("open", ""); }, }); Object.defineProperty(HTMLDialogElement.prototype, "close", { configurable: true, value(this: HTMLDialogElement) { this.removeAttribute("open"); this.dispatchEvent(new Event("close")); }, }); vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => { callback(0); return 1; }); }); afterEach(() => { vi.unstubAllGlobals(); Object.defineProperty(HTMLDialogElement.prototype, "showModal", { configurable: true, value: originalShowModal, }); Object.defineProperty(HTMLDialogElement.prototype, "close", { configurable: true, value: originalClose, }); }); async function renderShell(initialEntry = "/projects") { const techLog = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input; const router = createMemoryRouter( [ { path: "*", element: (
공개 본문
), }, ], { initialEntries: [initialEntry] }, ); const view = render( renderWithQueryProviders( , )); // 포트가 async 가 되면서 첫 페인트에는 데이터가 없다. 화면이 정착한 뒤 // 단언하도록 여기서 한 번 기다린다 — 각 테스트에 흩어 놓으면 빠뜨린 곳이 생긴다. await screen.findByRole("main"); return { ...view, router }; } describe("TechLog Public shell", () => { it("preserves source landmark order, navigation copy, current state, and footer", async () => { const view = await renderShell(); const frame = view.container.querySelector(".site-frame"); expect( Array.from(frame?.children ?? [], (child) => child.tagName), ).toEqual(["A", "HEADER", "MAIN", "FOOTER"]); expect(screen.getByRole("link", { name: "본문으로 건너뛰기" })).toHaveAttribute( "href", "#main-content", ); expect(screen.getByRole("banner")).toHaveClass("site-header"); expect(screen.getByRole("main")).toHaveAttribute("id", "main-content"); expect(screen.getByRole("contentinfo")).toHaveClass("site-footer"); expect(screen.getByRole("navigation", { name: "주요 탐색" })).toBeVisible(); expect( screen.getByRole("navigation", { name: "모바일 주요 탐색" }), ).toBeInTheDocument(); const expectedNavigation = [ ["탐색", "/explore"], ["프로젝트", "/projects"], ["변경 기록", "/releases"], ["프로필", "/profile"], ]; expect( Array.from( view.container.querySelectorAll(".desktop-nav a"), (link) => [link.textContent, link.getAttribute("href")], ), ).toEqual(expectedNavigation); expect( Array.from( view.container.querySelectorAll( ".mobile-nav nav a", ), (link) => [link.textContent, link.getAttribute("href")], ), ).toEqual(expectedNavigation); for (const link of screen.getAllByRole("link", { name: "프로젝트" })) { expect(link).toHaveAttribute("aria-current", "page"); } expect(screen.getByText("동현")).toHaveClass("footer-name"); expect( screen.getByText("문제를 재현하고 검증해 운영 가능한 설계로 연결합니다."), ).toBeVisible(); expect(view.container.querySelector(".app-shell")).toBeNull(); }); it("uses the canonical root route for brand navigation", async () => { const user = userEvent.setup(); const { router } = await renderShell("/projects/backend-skeleton"); await user.click(screen.getByRole("link", { name: "TechLog 홈" })); expect(router.state.location.pathname).toBe("/"); }); it("opens search by click, closes on Escape, and restores trigger focus", async () => { const user = userEvent.setup(); await renderShell(); const trigger = screen.getByRole("button", { name: "TechLog 검색 열기" }); await user.click(trigger); const dialog = screen.getByRole("dialog", { name: "TechLog 검색" }); const input = within(dialog).getByRole("searchbox", { name: "검색어" }); expect(dialog).toHaveAttribute("open"); expect(dialog).toHaveAttribute("aria-labelledby", `${dialog.id}-title`); expect(input).toHaveFocus(); await user.keyboard("{Escape}"); expect(dialog).not.toHaveAttribute("open"); expect(trigger).toHaveFocus(); }); it("opens the native search trigger from the keyboard", async () => { const user = userEvent.setup(); await renderShell(); const trigger = screen.getByRole("button", { name: "TechLog 검색 열기" }); trigger.focus(); await user.keyboard("{Enter}"); expect(screen.getByRole("dialog", { name: "TechLog 검색" })).toHaveAttribute( "open", ); expect(screen.getByRole("searchbox", { name: "검색어" })).toHaveFocus(); }); it("returns source-equivalent results and navigates to their canonical route", async () => { const user = userEvent.setup(); const { router } = await renderShell(); await user.click( screen.getByRole("button", { name: "TechLog 검색 열기" }), ); const input = screen.getByRole("searchbox", { name: "검색어" }); await user.type(input, "Keycloak"); expect(screen.getByText("1개의 공개 기록")).toBeVisible(); const result = screen.getByRole("link", { name: /Auth Lab/ }); expect(result).toHaveAttribute("href", "/projects/auth-lab"); expect( screen.getByRole("link", { name: /전체 검색 결과 보기/ }), ).toHaveAttribute("href", "/search?q=Keycloak"); await user.click(result); expect(router.state.location.pathname).toBe("/projects/auth-lab"); }); it("navigates the full-search action with its canonical query intact", async () => { const user = userEvent.setup(); const { router } = await renderShell(); await user.click( screen.getByRole("button", { name: "TechLog 검색 열기" }), ); await user.type( screen.getByRole("searchbox", { name: "검색어" }), "Keycloak", ); await user.click( screen.getByRole("link", { name: /전체 검색 결과 보기/ }), ); expect(router.state.location.pathname).toBe("/search"); expect(router.state.location.search).toBe("?q=Keycloak"); }); it("uses native mobile disclosure activation and closes it after selection", async () => { const user = userEvent.setup(); const view = await renderShell(); const details = view.container.querySelector( "details.mobile-nav", ); const summary = details?.querySelector("summary"); expect(details).not.toBeNull(); expect(summary).not.toBeNull(); await user.click(summary!); expect(details).toHaveAttribute("open"); await user.click( within(details!).getByRole("link", { name: "탐색" }), ); expect(details).not.toHaveAttribute("open"); await user.click(summary!); expect(details).toHaveAttribute("open"); summary!.focus(); await user.keyboard("{Escape}"); expect(details).not.toHaveAttribute("open"); expect(summary).toHaveFocus(); }); it("closes a natively opened mobile disclosure before opening one search dialog", async () => { const user = userEvent.setup(); const view = await renderShell(); const details = view.container.querySelector( "details.mobile-nav", ); const summary = details?.querySelector("summary"); expect(details).not.toBeNull(); expect(summary).not.toBeNull(); await user.click(summary!); expect(details).toHaveAttribute("open"); await user.click( screen.getByRole("button", { name: "TechLog 검색 열기" }), ); expect(details).not.toHaveAttribute("open"); expect(screen.getAllByRole("dialog", { hidden: true })).toHaveLength(1); }); }); describe("TechLog fatal Public state", () => { it("preserves error copy, trace relationship, and retry variants", async () => { const user = userEvent.setup(); const retry = vi.fn(); const view = render( , ); expect(screen.getByRole("main")).toHaveClass("shell", "fatal-state"); expect( screen.getByRole("heading", { name: "페이지를 불러오지 못했습니다." }), ).toBeVisible(); expect(view.container.querySelector(".trace-id code")).toHaveTextContent( "PUBLIC-500", ); await user.click(screen.getByRole("button", { name: "다시 시도" })); expect(retry).toHaveBeenCalledOnce(); }); });