1
0
forked from baron/baron-sso

조직도 표현 개선

This commit is contained in:
2026-05-29 10:33:15 +09:00
parent 6a6730b544
commit c489c7c38f
34 changed files with 1872 additions and 391 deletions

View File

@@ -0,0 +1,148 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createI18nMock } from "../../test/i18nMock";
import UserListPage from "./UserListPage";
const selectRenderCounter = vi.hoisted(() => ({ count: 0 }));
const users = Array.from({ length: 200 }, (_, index) => ({
id: `user-${index}`,
name: `User ${index}`,
email: `user${index}@example.com`,
phone: `010-${String(index).padStart(4, "0")}-0000`,
role: "user",
status: "active",
tenantSlug: "hanmac",
tenant: { id: "tenant-1", name: "한맥", slug: "hanmac" },
metadata: {},
createdAt: "2026-05-01T00:00:00Z",
updatedAt: "2026-05-01T00:00:00Z",
}));
const fetchUsersMock = vi.hoisted(() => vi.fn());
vi.mock("../../lib/i18n", () => createI18nMock());
vi.mock("../../lib/adminApi", () => ({
fetchMe: vi.fn(async () => ({
id: "admin-user",
role: "super_admin",
name: "Admin",
email: "admin@example.com",
})),
fetchAllTenants: vi.fn(async () => ({
items: [{ id: "tenant-1", name: "한맥", slug: "hanmac" }],
total: 1,
})),
fetchTenant: vi.fn(async () => ({
id: "tenant-1",
name: "한맥",
slug: "hanmac",
config: { userSchema: [] },
})),
fetchUsers: fetchUsersMock,
bulkCreateUsers: vi.fn(),
bulkDeleteUsers: vi.fn(),
bulkUpdateUsers: vi.fn(),
deleteUser: vi.fn(),
exportUsersCSV: vi.fn(),
updateUser: vi.fn(),
}));
vi.mock("../../components/ui/select", () => ({
Select: ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
),
SelectTrigger: ({
children,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement>) => {
selectRenderCounter.count += 1;
return (
<button type="button" {...props}>
{children}
</button>
);
},
SelectValue: () => <span />,
SelectContent: ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
),
SelectItem: ({
children,
value: _value,
}: {
children: React.ReactNode;
value: string;
}) => <div>{children}</div>,
}));
function renderUserListPage() {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<UserListPage />
</MemoryRouter>
</QueryClientProvider>,
);
}
describe("UserListPage search rendering", () => {
beforeEach(() => {
selectRenderCounter.count = 0;
fetchUsersMock.mockReset();
fetchUsersMock.mockImplementation(
async (
_limit: number,
_offset: number,
search?: string,
) => {
const normalizedSearch = search?.trim().toLowerCase();
const items = normalizedSearch
? users.filter((user) =>
`${user.name} ${user.email}`
.toLowerCase()
.includes(normalizedSearch),
)
: users;
return { items, total: items.length };
},
);
});
it("does not rerender user table controls while typing a draft search", async () => {
renderUserListPage();
await screen.findByText("User 199");
const searchInput = screen.getByPlaceholderText("이름 또는 이메일 검색...");
const renderCountBeforeTyping = selectRenderCounter.count;
fireEvent.change(searchInput, { target: { value: "u" } });
expect(searchInput).toHaveValue("u");
expect(selectRenderCounter.count).toBe(renderCountBeforeTyping);
});
it("renders a 200-user search result update within 200ms after search submit", async () => {
renderUserListPage();
await screen.findByText("User 199");
const searchInput = screen.getByPlaceholderText("이름 또는 이메일 검색...");
const startedAt = performance.now();
fireEvent.change(searchInput, { target: { value: "user 19" } });
fireEvent.keyDown(searchInput, { key: "Enter" });
await screen.findByText("User 19");
await waitFor(() => {
expect(screen.queryByText("User 0")).not.toBeInTheDocument();
});
expect(performance.now() - startedAt).toBeLessThan(200);
});
});