Files
tech-log-frontend/tests/component/design-system-platform.test.tsx

270 lines
8.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @vitest-environment jsdom
import {
act,
fireEvent,
render,
screen,
waitFor,
within,
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
import { describe, expect, it, vi } from "vitest";
import {
Button,
Checkbox,
DataTable,
Drawer,
IconButton,
Menu,
MenuIcon,
Pagination,
Popover,
RadioGroup,
Switch,
Tabs,
ToastProvider,
useToast,
} from "../../src/presentation/design-system/index.ts";
describe("design-system platform interactions", () => {
it("keeps decorative icons out of the accessibility tree and names icon actions", () => {
render(
<>
<MenuIcon />
<IconButton accessibleName="탐색 열기">
<MenuIcon />
</IconButton>
</>,
);
expect(screen.getByRole("button", { name: "탐색 열기" })).toBeVisible();
expect(screen.queryByRole("img")).not.toBeInTheDocument();
});
it("supports native choices, indeterminate state, radio arrows and switches", async () => {
const user = userEvent.setup();
const onRadio = vi.fn();
const onSwitch = vi.fn();
render(
<>
<Checkbox indeterminate label="일부 선택" />
<RadioGroup
label="밀도"
name="density"
onChange={onRadio}
options={[
{ value: "normal", label: "보통" },
{ value: "compact", label: "조밀" },
]}
value="normal"
/>
<Switch
checked={false}
label="알림"
onChange={onSwitch}
/>
</>,
);
const checkbox = screen.getByRole("checkbox", { name: "일부 선택" });
expect(checkbox).toBePartiallyChecked();
const normal = screen.getByRole("radio", { name: "보통" });
normal.focus();
await user.keyboard("{ArrowRight}");
expect(onRadio).toHaveBeenCalledWith("compact");
await user.click(screen.getByRole("switch", { name: "알림" }));
expect(onSwitch).toHaveBeenCalledWith(true);
});
it("moves through a menu, executes once, dismisses and restores focus", async () => {
const user = userEvent.setup();
const inspect = vi.fn();
render(
<Menu
items={[
{ id: "open", label: "열기", onSelect: vi.fn() },
{ id: "inspect", label: "검사", onSelect: inspect },
]}
triggerLabel="작업"
/>,
);
const trigger = screen.getByRole("button", { name: "작업" });
trigger.focus();
await user.keyboard("{ArrowDown}");
expect(screen.getByRole("menuitem", { name: "열기" })).toHaveFocus();
await user.keyboard("{ArrowDown}{Enter}");
expect(inspect).toHaveBeenCalledOnce();
await waitFor(() => expect(trigger).toHaveFocus());
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
});
it("supports manual tab activation with arrow-key roving focus", async () => {
const user = userEvent.setup();
render(
<Tabs
activation="manual"
label="계층"
tabs={[
{ id: "tokens", label: "토큰", panel: "토큰 내용" },
{ id: "patterns", label: "패턴", panel: "패턴 내용" },
]}
/>,
);
const tokenTab = screen.getByRole("tab", { name: "토큰" });
tokenTab.focus();
await user.keyboard("{ArrowRight}");
const patternTab = screen.getByRole("tab", { name: "패턴" });
expect(patternTab).toHaveFocus();
expect(patternTab).toHaveAttribute("aria-selected", "false");
await user.keyboard("{Enter}");
expect(patternTab).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("tabpanel", { name: "패턴" })).toHaveTextContent(
"패턴 내용",
);
});
it("uses a modal drawer and restores focus after Escape", async () => {
const user = userEvent.setup();
function Harness() {
const [open, setOpen] = useState(false);
return (
<>
<Button onClick={() => setOpen(true)}>탐색 열기</Button>
<Drawer onClose={() => setOpen(false)} open={open} title="탐색">
<a href="/target">대상</a>
</Drawer>
</>
);
}
render(<Harness />);
const trigger = screen.getByRole("button", { name: "탐색 열기" });
await user.click(trigger);
expect(screen.getByRole("dialog", { name: "탐색" })).toHaveAttribute("open");
await user.keyboard("{Escape}");
await waitFor(() => expect(trigger).toHaveFocus());
});
it("bounds the toast queue and collapses duplicate IDs", async () => {
const user = userEvent.setup();
function Harness() {
const toast = useToast();
return (
<>
<Button
onClick={() =>
toast.push({ id: "same", title: "저장됨", durationMs: 60_000 })
}
>
중복
</Button>
<Button
onClick={() => {
for (let index = 0; index < 5; index += 1) {
toast.push({
id: `toast-${index}`,
title: `알림 ${index}`,
durationMs: 60_000,
});
}
}}
>
다섯
</Button>
</>
);
}
render(
<ToastProvider limit={3}>
<Harness />
</ToastProvider>,
);
await user.click(screen.getByRole("button", { name: "중복" }));
await user.click(screen.getByRole("button", { name: "중복" }));
expect(screen.getByText("×2")).toBeVisible();
await user.click(screen.getByRole("button", { name: "다섯 개" }));
const region = screen.getByRole("region", { name: "알림" });
expect(within(region).getAllByRole("article")).toHaveLength(3);
expect(within(region).queryByText("알림 0")).not.toBeInTheDocument();
expect(within(region).getByText("알림 4")).toBeVisible();
});
it("pauses toast timeout while the user is interacting", () => {
vi.useFakeTimers();
function Harness() {
const toast = useToast();
return (
<Button
onClick={() =>
toast.push({ id: "timed", title: "시간 제한", durationMs: 1000 })
}
>
표시
</Button>
);
}
render(
<ToastProvider>
<Harness />
</ToastProvider>,
);
fireEvent.click(screen.getByRole("button", { name: "표시" }));
const item = screen.getByRole("article");
fireEvent.mouseEnter(item);
act(() => vi.advanceTimersByTime(2000));
expect(item).toBeVisible();
fireEvent.mouseLeave(item);
act(() => vi.advanceTimersByTime(1000));
expect(screen.queryByText("시간 제한")).not.toBeInTheDocument();
vi.useRealTimers();
});
it("provides dismissible popover and data/navigation patterns", async () => {
const user = userEvent.setup();
const onPage = vi.fn();
render(
<>
<Popover triggerLabel="설명 열기">
<Button>내부 작업</Button>
</Popover>
<DataTable
caption="결과"
columns={[
{ id: "name", header: "이름", cell: (row) => row.name },
]}
empty="비어 있음"
rowKey={(row) => row.id}
rows={[{ id: "one", name: "첫 항목" }]}
/>
<Pagination
label="페이지"
nextLabel="다음"
onChange={onPage}
page={1}
pageCount={2}
pageLabel={(page) => `${page}페이지`}
previousLabel="이전"
/>
</>,
);
const popoverTrigger = screen.getByRole("button", { name: "설명 열기" });
await user.click(popoverTrigger);
expect(screen.getByRole("dialog")).toHaveTextContent("내부 작업");
await user.keyboard("{Escape}");
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
expect(popoverTrigger).toHaveFocus();
expect(screen.getByRole("table", { name: "결과" })).toHaveTextContent(
"첫 항목",
);
await user.click(screen.getByRole("link", { name: "2페이지" }));
expect(onPage).toHaveBeenCalledWith(2);
});
});