270 lines
8.0 KiB
TypeScript
270 lines
8.0 KiB
TypeScript
// @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.js";
|
||
|
||
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: "Notifications" });
|
||
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);
|
||
});
|
||
});
|