From 3bc74e01956d8ad9931dbe8de0341da68bf92331 Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Sat, 15 Aug 2026 21:42:49 +0900 Subject: [PATCH] test: strengthen TechLog shell contracts --- tests/features/tech-log/public-shell.test.tsx | 65 ++++++++++++++++--- .../features/tech-log/style-contract.test.ts | 40 ++++++++++++ 2 files changed, 96 insertions(+), 9 deletions(-) diff --git a/tests/features/tech-log/public-shell.test.tsx b/tests/features/tech-log/public-shell.test.tsx index fd4eb55..3cc4ff9 100644 --- a/tests/features/tech-log/public-shell.test.tsx +++ b/tests/features/tech-log/public-shell.test.tsx @@ -1,6 +1,6 @@ // @vitest-environment jsdom -import { fireEvent, render, screen, within } from "@testing-library/react"; +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"; @@ -94,20 +94,26 @@ describe("TechLog Public shell", () => { 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) => [link.textContent, link.getAttribute("href")], ), - ).toEqual(["탐색", "프로젝트", "변경 기록", "프로필"]); + ).toEqual(expectedNavigation); expect( Array.from( view.container.querySelectorAll( ".mobile-nav nav a", ), - (link) => link.textContent, + (link) => [link.textContent, link.getAttribute("href")], ), - ).toEqual(["탐색", "프로젝트", "변경 기록", "프로필"]); + ).toEqual(expectedNavigation); for (const link of screen.getAllByRole("link", { name: "프로젝트" })) { expect(link).toHaveAttribute("aria-current", "page"); } @@ -182,7 +188,26 @@ describe("TechLog Public shell", () => { expect(router.state.location.pathname).toBe("/projects/auth-lab"); }); - it("closes mobile navigation on Escape and before opening the one search dialog", async () => { + it("navigates the full-search action with its canonical query intact", async () => { + const user = userEvent.setup(); + const { router } = 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 = renderShell(); const details = view.container.querySelector( @@ -192,12 +217,34 @@ describe("TechLog Public shell", () => { expect(details).not.toBeNull(); expect(summary).not.toBeNull(); - details!.open = true; - fireEvent.keyDown(details!, { key: "Escape" }); + 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(); + }); - details!.open = true; + it("closes a natively opened mobile disclosure before opening one search dialog", async () => { + const user = userEvent.setup(); + const view = 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 검색 열기" }), ); diff --git a/tests/features/tech-log/style-contract.test.ts b/tests/features/tech-log/style-contract.test.ts index 14e8764..749a4e7 100644 --- a/tests/features/tech-log/style-contract.test.ts +++ b/tests/features/tech-log/style-contract.test.ts @@ -1,6 +1,7 @@ // @vitest-environment jsdom import { render } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { createElement } from "react"; @@ -64,6 +65,7 @@ describe("TechLog consumer-visible style contract", () => { const headerInner = view.container.querySelector( ".header-inner", ); + const main = view.getByRole("main"); const wordmark = view.getByRole("link", { name: "TechLog 홈" }); expect(rootStyle.getPropertyValue("--canvas")).toBe("#f7f7f5"); @@ -71,6 +73,13 @@ describe("TechLog consumer-visible style contract", () => { expect(rootStyle.getPropertyValue("--signal")).toBe("#3e5cc7"); expect(rootStyle.getPropertyValue("--shell")).toBe("1180px"); expect(rootStyle.getPropertyValue("--body-copy")).toBe("42rem"); + expect(rootStyle.color).toBe("var(--ink)"); + expect(rootStyle.background).toBe("var(--canvas)"); + expect(bodyStyle.color).toBe("var(--ink)"); + expect(bodyStyle.background).toContain( + "linear-gradient(to bottom, rgba(255, 255, 255, 0.45), transparent 420px)", + ); + expect(bodyStyle.background).toContain("var(--canvas)"); expect(bodyStyle.fontFamily).toContain("Pretendard Variable"); expect(bodyStyle.fontSize).toBe("16px"); expect(bodyStyle.fontWeight).toBe("400"); @@ -80,10 +89,41 @@ describe("TechLog consumer-visible style contract", () => { expect(getComputedStyle(headerInner!).display).toBe("grid"); expect(getComputedStyle(headerInner!).minHeight).toBe("76px"); expect(getComputedStyle(headerInner!).gap).toBe("48px"); + expect(getComputedStyle(main).width).toBe( + "min(var(--shell), calc(100% - 80px))", + ); expect(getComputedStyle(wordmark).fontSize).toBe("17px"); expect(getComputedStyle(wordmark).fontWeight).toBe("680"); }); + it("preserves keyboard focus and the real search-field focus relationship", async () => { + const user = userEvent.setup(); + const view = renderPublicSurface( + createElement( + "main", + { id: "main-content", className: "shell" }, + "공개 본문", + ), + ); + const wordmark = view.getByRole("link", { name: "TechLog 홈" }); + const searchInput = view.container.querySelector( + ".search-field input", + ); + const searchField = view.container.querySelector( + ".search-field", + ); + + await user.tab(); + await user.tab(); + expect(wordmark).toHaveFocus(); + + searchInput!.focus(); + + expect(searchInput).toHaveFocus(); + expect(searchField).not.toBeNull(); + expect(searchField!.matches(":focus-within")).toBe(true); + }); + it("keeps every header and dialog control at the source minimum target size", () => { const view = renderPublicSurface( createElement(