test: strengthen TechLog shell contracts
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
// @vitest-environment jsdom
|
// @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 userEvent from "@testing-library/user-event";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { createMemoryRouter, RouterProvider } from "react-router-dom";
|
import { createMemoryRouter, RouterProvider } from "react-router-dom";
|
||||||
@@ -94,20 +94,26 @@ describe("TechLog Public shell", () => {
|
|||||||
expect(
|
expect(
|
||||||
screen.getByRole("navigation", { name: "모바일 주요 탐색" }),
|
screen.getByRole("navigation", { name: "모바일 주요 탐색" }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
const expectedNavigation = [
|
||||||
|
["탐색", "/explore"],
|
||||||
|
["프로젝트", "/projects"],
|
||||||
|
["변경 기록", "/releases"],
|
||||||
|
["프로필", "/profile"],
|
||||||
|
];
|
||||||
expect(
|
expect(
|
||||||
Array.from(
|
Array.from(
|
||||||
view.container.querySelectorAll<HTMLAnchorElement>(".desktop-nav a"),
|
view.container.querySelectorAll<HTMLAnchorElement>(".desktop-nav a"),
|
||||||
(link) => link.textContent,
|
(link) => [link.textContent, link.getAttribute("href")],
|
||||||
),
|
),
|
||||||
).toEqual(["탐색", "프로젝트", "변경 기록", "프로필"]);
|
).toEqual(expectedNavigation);
|
||||||
expect(
|
expect(
|
||||||
Array.from(
|
Array.from(
|
||||||
view.container.querySelectorAll<HTMLAnchorElement>(
|
view.container.querySelectorAll<HTMLAnchorElement>(
|
||||||
".mobile-nav nav a",
|
".mobile-nav nav a",
|
||||||
),
|
),
|
||||||
(link) => link.textContent,
|
(link) => [link.textContent, link.getAttribute("href")],
|
||||||
),
|
),
|
||||||
).toEqual(["탐색", "프로젝트", "변경 기록", "프로필"]);
|
).toEqual(expectedNavigation);
|
||||||
for (const link of screen.getAllByRole("link", { name: "프로젝트" })) {
|
for (const link of screen.getAllByRole("link", { name: "프로젝트" })) {
|
||||||
expect(link).toHaveAttribute("aria-current", "page");
|
expect(link).toHaveAttribute("aria-current", "page");
|
||||||
}
|
}
|
||||||
@@ -182,7 +188,26 @@ describe("TechLog Public shell", () => {
|
|||||||
expect(router.state.location.pathname).toBe("/projects/auth-lab");
|
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 user = userEvent.setup();
|
||||||
const view = renderShell();
|
const view = renderShell();
|
||||||
const details = view.container.querySelector<HTMLDetailsElement>(
|
const details = view.container.querySelector<HTMLDetailsElement>(
|
||||||
@@ -192,12 +217,34 @@ describe("TechLog Public shell", () => {
|
|||||||
expect(details).not.toBeNull();
|
expect(details).not.toBeNull();
|
||||||
expect(summary).not.toBeNull();
|
expect(summary).not.toBeNull();
|
||||||
|
|
||||||
details!.open = true;
|
await user.click(summary!);
|
||||||
fireEvent.keyDown(details!, { key: "Escape" });
|
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(details).not.toHaveAttribute("open");
|
||||||
expect(summary).toHaveFocus();
|
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<HTMLDetailsElement>(
|
||||||
|
"details.mobile-nav",
|
||||||
|
);
|
||||||
|
const summary = details?.querySelector<HTMLElement>("summary");
|
||||||
|
expect(details).not.toBeNull();
|
||||||
|
expect(summary).not.toBeNull();
|
||||||
|
|
||||||
|
await user.click(summary!);
|
||||||
|
expect(details).toHaveAttribute("open");
|
||||||
await user.click(
|
await user.click(
|
||||||
screen.getByRole("button", { name: "TechLog 검색 열기" }),
|
screen.getByRole("button", { name: "TechLog 검색 열기" }),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
|
|
||||||
import { render } from "@testing-library/react";
|
import { render } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
import { createElement } from "react";
|
import { createElement } from "react";
|
||||||
@@ -64,6 +65,7 @@ describe("TechLog consumer-visible style contract", () => {
|
|||||||
const headerInner = view.container.querySelector<HTMLElement>(
|
const headerInner = view.container.querySelector<HTMLElement>(
|
||||||
".header-inner",
|
".header-inner",
|
||||||
);
|
);
|
||||||
|
const main = view.getByRole("main");
|
||||||
const wordmark = view.getByRole("link", { name: "TechLog 홈" });
|
const wordmark = view.getByRole("link", { name: "TechLog 홈" });
|
||||||
|
|
||||||
expect(rootStyle.getPropertyValue("--canvas")).toBe("#f7f7f5");
|
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("--signal")).toBe("#3e5cc7");
|
||||||
expect(rootStyle.getPropertyValue("--shell")).toBe("1180px");
|
expect(rootStyle.getPropertyValue("--shell")).toBe("1180px");
|
||||||
expect(rootStyle.getPropertyValue("--body-copy")).toBe("42rem");
|
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.fontFamily).toContain("Pretendard Variable");
|
||||||
expect(bodyStyle.fontSize).toBe("16px");
|
expect(bodyStyle.fontSize).toBe("16px");
|
||||||
expect(bodyStyle.fontWeight).toBe("400");
|
expect(bodyStyle.fontWeight).toBe("400");
|
||||||
@@ -80,10 +89,41 @@ describe("TechLog consumer-visible style contract", () => {
|
|||||||
expect(getComputedStyle(headerInner!).display).toBe("grid");
|
expect(getComputedStyle(headerInner!).display).toBe("grid");
|
||||||
expect(getComputedStyle(headerInner!).minHeight).toBe("76px");
|
expect(getComputedStyle(headerInner!).minHeight).toBe("76px");
|
||||||
expect(getComputedStyle(headerInner!).gap).toBe("48px");
|
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).fontSize).toBe("17px");
|
||||||
expect(getComputedStyle(wordmark).fontWeight).toBe("680");
|
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<HTMLInputElement>(
|
||||||
|
".search-field input",
|
||||||
|
);
|
||||||
|
const searchField = view.container.querySelector<HTMLElement>(
|
||||||
|
".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", () => {
|
it("keeps every header and dialog control at the source minimum target size", () => {
|
||||||
const view = renderPublicSurface(
|
const view = renderPublicSurface(
|
||||||
createElement(
|
createElement(
|
||||||
|
|||||||
Reference in New Issue
Block a user