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 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> ) { const onChange = jest.fn() renderWithProviders( ) 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(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 }) }) describe('paste multiple attendees', () => { function setupFreeSolo(selectedUsers: User[] = []) { const onChange = jest.fn() renderWithProviders( ) return { onChange } } it('splits pasted comma-separated emails into individual attendees', async () => { const { onChange } = setupFreeSolo() const input = screen.getByRole('combobox') fireEvent.paste(input, { clipboardData: { getData: () => 'alice@example.com, bob@example.com' } }) await waitFor(() => { expect(onChange).toHaveBeenCalledWith( expect.anything(), expect.arrayContaining([ expect.objectContaining({ email: 'alice@example.com' }), expect.objectContaining({ email: 'bob@example.com' }) ]) ) }) }) it('splits pasted semicolon-separated emails', async () => { const { onChange } = setupFreeSolo() const input = screen.getByRole('combobox') fireEvent.paste(input, { clipboardData: { getData: () => 'alice@example.com;bob@example.com' } }) await waitFor(() => { expect(onChange).toHaveBeenCalledWith( expect.anything(), expect.arrayContaining([ expect.objectContaining({ email: 'alice@example.com' }), expect.objectContaining({ email: 'bob@example.com' }) ]) ) }) }) it('splits pasted newline-separated emails', async () => { const { onChange } = setupFreeSolo() const input = screen.getByRole('combobox') fireEvent.paste(input, { clipboardData: { getData: () => 'alice@example.com\nbob@example.com' } }) await waitFor(() => { expect(onChange).toHaveBeenCalledWith( expect.anything(), expect.arrayContaining([ expect.objectContaining({ email: 'alice@example.com' }), expect.objectContaining({ email: 'bob@example.com' }) ]) ) }) }) it('splits pasted space-separated emails', async () => { const { onChange } = setupFreeSolo() const input = screen.getByRole('combobox') fireEvent.paste(input, { clipboardData: { getData: () => 'alice@example.com bob@example.com' } }) await waitFor(() => { expect(onChange).toHaveBeenCalledWith( expect.anything(), expect.arrayContaining([ expect.objectContaining({ email: 'alice@example.com' }), expect.objectContaining({ email: 'bob@example.com' }) ]) ) }) }) it('skips duplicate emails already in selectedUsers', async () => { const existing: User = { email: 'alice@example.com', displayName: 'Alice' } const { onChange } = setupFreeSolo([existing]) const input = screen.getByRole('combobox') fireEvent.paste(input, { clipboardData: { getData: () => 'alice@example.com, bob@example.com' } }) await waitFor(() => { expect(onChange).toHaveBeenCalledWith( expect.anything(), expect.arrayContaining([ existing, expect.objectContaining({ email: 'bob@example.com' }) ]) ) // Should NOT have alice duplicated const call = onChange.mock.calls[0][1] const aliceCount = call.filter( (u: User) => u.email === 'alice@example.com' ).length expect(aliceCount).toBe(1) }) }) it('leaves invalid text in input when some emails are invalid', async () => { const { onChange } = setupFreeSolo() const input = screen.getByRole('combobox') fireEvent.paste(input, { clipboardData: { getData: () => 'alice@example.com, not-an-email, bob@example.com' } }) await waitFor(() => { expect(onChange).toHaveBeenCalledWith( expect.anything(), expect.arrayContaining([ expect.objectContaining({ email: 'alice@example.com' }), expect.objectContaining({ email: 'bob@example.com' }) ]) ) }) }) it('does not intercept paste of a single email', async () => { const { onChange } = setupFreeSolo() const input = screen.getByRole('combobox') fireEvent.paste(input, { clipboardData: { getData: () => 'alice@example.com' } }) // Single email should NOT trigger multi-paste handler expect(onChange).not.toHaveBeenCalled() }) }) it('retains input value when field loses focus (blur)', async () => { let resolveSearch: (value: User[]) => void const searchPromise = new Promise(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') }) }) })