feat: provide reusable UI and state galleries

This commit is contained in:
donghyeon-ka
2026-07-25 23:59:12 +09:00
parent a8e3db1aec
commit 3581ead595
17 changed files with 1167 additions and 34 deletions
+8 -2
View File
@@ -59,8 +59,14 @@ describe("async UI state matrix", () => {
const state = deriveAsyncState({ failure });
render(<AsyncSurface state={state} />);
expect(screen.getByRole("alert")).toHaveTextContent(failure.userMessageKey);
expect(screen.getByRole("button")).toHaveTextContent("retry");
expect(screen.getByRole("alert")).toHaveTextContent(
"요청을 완료하지 못했습니다.",
);
expect(screen.getByRole("alert")).toHaveAttribute(
"data-message-key",
failure.userMessageKey,
);
expect(screen.getByRole("button")).toHaveTextContent("다시 시도");
expect(screen.getByRole("alert")).not.toHaveTextContent("stack");
});
+7 -1
View File
@@ -44,7 +44,13 @@ describe("render recovery boundaries", () => {
<AsyncSurface state={state} />
</FeatureBoundary>,
);
expect(screen.getByRole("alert")).toHaveTextContent("error.server_failure");
expect(screen.getByRole("alert")).toHaveTextContent(
"요청을 완료하지 못했습니다.",
);
expect(screen.getByRole("alert")).toHaveAttribute(
"data-message-key",
"error.server_failure",
);
});
it("renders a safe boot shell with no endpoint or stack", () => {
@@ -44,6 +44,11 @@ describe("removable sample feature page", () => {
};
render(<SampleResourcePage facade={facade} />);
expect(await screen.findByRole("alert")).toHaveTextContent("error.server_failure");
const alert = await screen.findByRole("alert");
expect(alert).toHaveTextContent("요청을 완료하지 못했습니다.");
expect(alert).toHaveAttribute(
"data-message-key",
"error.server_failure",
);
});
});
+108
View File
@@ -0,0 +1,108 @@
// @vitest-environment jsdom
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
import { describe, expect, it, vi } from "vitest";
import { Alert } from "../../src/presentation/components/ui/alert.jsx";
import { Badge } from "../../src/presentation/components/ui/badge.jsx";
import { Button } from "../../src/presentation/components/ui/button.jsx";
import { Card } from "../../src/presentation/components/ui/card.jsx";
import { Dialog } from "../../src/presentation/components/ui/dialog.jsx";
import { TextField } from "../../src/presentation/components/ui/text-field.jsx";
describe("domain-neutral UI primitives", () => {
it("connects field help and validation errors to the input", () => {
render(
<TextField
label="이름"
description="표시할 이름입니다."
error="이름을 입력해 주세요."
required
/>,
);
const field = screen.getByRole("textbox", { name: "이름" });
expect(field).toBeRequired();
expect(field).toHaveAccessibleDescription(
"표시할 이름입니다. 이름을 입력해 주세요.",
);
expect(field).toHaveAttribute("aria-invalid", "true");
});
it("exposes semantic variants without changing native button behavior", async () => {
const user = userEvent.setup();
const onClick = vi.fn();
render(
<>
<Button variant="danger" onClick={onClick}>
제거
</Button>
<Button disabled>사용 불가</Button>
</>,
);
await user.click(screen.getByRole("button", { name: "제거" }));
expect(onClick).toHaveBeenCalledOnce();
expect(screen.getByRole("button", { name: "제거" })).toHaveClass(
"ui-button--danger",
);
expect(screen.getByRole("button", { name: "사용 불가" })).toBeDisabled();
});
it("labels cards, alerts, and badges with visible content", async () => {
const user = userEvent.setup();
const dismiss = vi.fn();
render(
<Card title="상태 카드" footer={<Badge variant="success">준비됨</Badge>}>
<Alert title="저장됨" variant="success" onDismiss={dismiss}>
안전하게 반영했습니다.
</Alert>
</Card>,
);
expect(screen.getByRole("article", { name: "상태 카드" })).toBeVisible();
expect(screen.getByRole("status")).toHaveTextContent(
"저장됨안전하게 반영했습니다.",
);
expect(screen.getByText("준비됨")).toHaveClass("ui-badge--success");
await user.click(screen.getByRole("button", { name: "저장됨 알림 닫기" }));
expect(dismiss).toHaveBeenCalledOnce();
});
it("closes a modal and restores focus to its trigger", async () => {
const user = userEvent.setup();
function DialogHarness() {
const [open, setOpen] = useState(false);
return (
<>
<Button onClick={() => setOpen(true)}>모달 열기</Button>
<Dialog
open={open}
onClose={() => setOpen(false)}
title="연동 확인"
actions={<Button onClick={() => setOpen(false)}>확인</Button>}
>
안전한 설명
</Dialog>
</>
);
}
render(<DialogHarness />);
const trigger = screen.getByRole("button", { name: "모달 열기" });
await user.click(trigger);
expect(screen.getByRole("dialog", { name: "연동 확인" })).toHaveAttribute(
"open",
);
await user.click(screen.getByRole("button", { name: "확인" }));
await waitFor(() => expect(trigger).toHaveFocus());
expect(screen.getByRole("dialog", { hidden: true })).not.toHaveAttribute(
"open",
);
});
});
+34
View File
@@ -0,0 +1,34 @@
import { expect, test } from "@playwright/test";
test("validates and reports the common text-field flow", async ({ page }) => {
await page.goto("/examples/ui");
await page.getByRole("button", { name: "입력 확인" }).click();
const field = page.getByRole("textbox", { name: "프로젝트 이름" });
await expect(field).toHaveAttribute("aria-invalid", "true");
await expect(field).toHaveAccessibleDescription(
/프로젝트 이름을 입력해 주세요/,
);
await field.fill("Starter");
await page.getByRole("button", { name: "입력 확인" }).click();
await expect(page.getByText("“Starter” 입력을 확인했습니다.")).toBeVisible();
});
test("traps modal interaction and restores focus to the trigger", async ({
page,
}) => {
await page.goto("/examples/ui");
const trigger = page.getByRole("button", { name: "모달 열기" });
await trigger.click();
const dialog = page.getByRole("dialog", { name: "연동 확인" });
await expect(dialog).toBeVisible();
await expect(
dialog.getByRole("button", { name: "연동 확인 닫기" }),
).toBeFocused();
await page.keyboard.press("Escape");
await expect(dialog).toBeHidden();
await expect(trigger).toBeFocused();
});