Files
workavia-calendar-front/__test__/components/PeopleSearch.test.tsx
T
2026-01-20 17:33:30 +01:00

248 lines
6.6 KiB
TypeScript

import { PeopleSearch, User } from "@/components/Attendees/PeopleSearch";
import { searchUsers } from "@/features/User/userAPI";
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "../utils/Renderwithproviders";
jest.mock("@/features/User/userAPI");
const mockedSearchUsers = searchUsers as jest.MockedFunction<
typeof searchUsers
>;
describe("PeopleSearch", () => {
const baseUser: User = {
email: "test@example.com",
displayName: "Test User",
avatarUrl: "https://example.com/avatar.png",
openpaasId: "1234567890",
};
function setup(
selectedUsers: User[] = [],
props?: Partial<React.ComponentProps<typeof PeopleSearch>>
) {
const onChange = jest.fn();
renderWithProviders(
<PeopleSearch
objectTypes={["user"]}
selectedUsers={selectedUsers}
onChange={onChange}
{...props}
/>
);
return { onChange };
}
beforeEach(() => {
jest.useFakeTimers();
mockedSearchUsers.mockReset();
});
afterEach(() => {
jest.useRealTimers();
});
it("calls searchUsers after debounce when typing", async () => {
mockedSearchUsers.mockResolvedValueOnce([baseUser]);
setup();
const input = screen.getByRole("combobox");
await userEvent.type(input, "Test");
await act(async () => {
jest.advanceTimersByTime(300);
});
await waitFor(() => {
expect(mockedSearchUsers).toHaveBeenCalledWith("Test", ["user"]);
});
});
it("renders search results and allows selection", async () => {
mockedSearchUsers.mockResolvedValueOnce([baseUser]);
const { onChange } = setup();
const input = screen.getByRole("combobox");
await userEvent.type(input, "Test");
await act(async () => {
jest.advanceTimersByTime(300);
});
const option = await screen.findByText("Test User");
await userEvent.click(option);
await waitFor(() => {
expect(onChange).toHaveBeenCalled();
});
});
it("does not show already selected users in options", async () => {
mockedSearchUsers.mockResolvedValueOnce([baseUser]);
setup([baseUser]);
const input = screen.getByRole("combobox");
await userEvent.type(input, "Test");
await act(async () => {
jest.advanceTimersByTime(300);
});
await waitFor(() => {
expect(screen.queryByText("test@example.com")).not.toBeInTheDocument();
});
});
it("triggers onToggleEventPreview on Enter key press", () => {
const onToggleEventPreview = jest.fn();
setup([], { onToggleEventPreview });
const input = screen.getByRole("combobox");
fireEvent.keyDown(input, { key: "Enter" });
expect(onToggleEventPreview).toHaveBeenCalled();
});
it("respects disabled state", () => {
setup([], { disabled: true });
expect(screen.getByRole("combobox")).toBeDisabled();
});
it("no options doesn't show dropdown when input is empty", async () => {
mockedSearchUsers.mockResolvedValueOnce([baseUser]);
setup();
const input = screen.getByRole("combobox");
userEvent.type(input, "Test");
await act(async () => {
jest.advanceTimersByTime(300);
});
await waitFor(() => {
expect(screen.getByRole("listbox")).toBeInTheDocument();
});
userEvent.clear(input);
await waitFor(() => {
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
});
});
it("shows 'No results' when search succeeds but returns empty array", async () => {
mockedSearchUsers.mockResolvedValueOnce([]);
setup();
const input = screen.getByRole("combobox");
await userEvent.type(input, "Test");
await act(async () => {
jest.advanceTimersByTime(300);
await Promise.resolve();
await Promise.resolve();
});
const noResults = await screen.findByText(
"peopleSearch.noResults",
{},
{ timeout: 5000 }
);
expect(noResults).toBeInTheDocument();
});
it("does not clear options when search fails and shows error snackbar", async () => {
mockedSearchUsers.mockResolvedValueOnce([baseUser]);
setup();
const input = screen.getByRole("combobox");
await userEvent.type(input, "Test");
await act(async () => {
jest.advanceTimersByTime(300);
await Promise.resolve();
});
await waitFor(() => {
expect(screen.getByText("Test User")).toBeInTheDocument();
});
mockedSearchUsers.mockRejectedValueOnce(new Error("Network error"));
await userEvent.clear(input);
await userEvent.type(input, "Error");
await act(async () => {
jest.advanceTimersByTime(300);
await Promise.resolve();
});
const errorMessage = await screen.findByText("peopleSearch.searchError");
expect(errorMessage).toBeInTheDocument();
expect(screen.queryByText("Test User")).not.toBeInTheDocument();
mockedSearchUsers.mockResolvedValueOnce([baseUser]);
await userEvent.clear(input);
await userEvent.type(input, "Test");
await act(async () => {
jest.advanceTimersByTime(300);
await Promise.resolve();
});
await waitFor(() => {
expect(screen.getByText("Test User")).toBeInTheDocument();
});
});
it("shows loading text when searching", async () => {
let resolveSearch: (value: User[]) => void;
const searchPromise = new Promise<User[]>((resolve) => {
resolveSearch = resolve;
});
mockedSearchUsers.mockReturnValueOnce(searchPromise);
setup();
const input = screen.getByRole("combobox");
await userEvent.type(input, "Test");
await act(async () => {
jest.advanceTimersByTime(300);
});
const loadingText = await screen.findByText(
"peopleSearch.loading",
{},
{ timeout: 5000 }
);
expect(loadingText).toBeInTheDocument();
await act(async () => {
resolveSearch!([baseUser]);
await searchPromise;
});
});
it("retains input value when field loses focus (blur)", async () => {
let resolveSearch: (value: User[]) => void;
const searchPromise = new Promise<User[]>((resolve) => {
resolveSearch = resolve;
});
mockedSearchUsers.mockReturnValueOnce(searchPromise);
await act(async () => {
setup();
});
const input = screen.getByRole("combobox");
await act(async () => {
userEvent.type(input, "Test");
});
await waitFor(() => {
expect(mockedSearchUsers).toHaveBeenCalledWith("Test", ["user"]);
});
expect(input).toHaveValue("Test");
await act(async () => {
input.blur();
});
await waitFor(() => {
expect(input).toHaveValue("Test");
});
});
});