feat: provide reusable UI and state galleries
This commit is contained in:
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
Reference in New Issue
Block a user