test(friends): update tests for unified friend search UI

Replace old "Add Friend" form tests (placeholder/button/inline messages) with
new tests for the Find People panel: search input, Direct Add row, toast feedback,
and searchUsers debounce. Also fix ancillary mocks (discoverStore, authStore,
activityStore, ConnectInstanceModal, findExistingDmForUser) required by the
rewritten component.
This commit is contained in:
Jannis Braun
2026-03-25 02:06:01 +01:00
parent a9266a3ad8
commit 277b69a732
@@ -5,6 +5,7 @@ import { MemoryRouter } from 'react-router-dom';
import { FriendsPage } from './FriendsPage'; import { FriendsPage } from './FriendsPage';
import { useSocialStore, type TaggedFriend, type TaggedFriendRequest } from '../../stores/socialStore'; import { useSocialStore, type TaggedFriend, type TaggedFriendRequest } from '../../stores/socialStore';
import { useSpaceStore } from '../../stores/spaceStore'; import { useSpaceStore } from '../../stores/spaceStore';
import { useUIStore } from '../../stores/uiStore';
import type { Friend, FriendRequest } from '@backspace/shared'; import type { Friend, FriendRequest } from '@backspace/shared';
// Mock the mascot animation hook // Mock the mascot animation hook
@@ -36,17 +37,80 @@ vi.mock('../../api/client', () => ({
cancelRequest: vi.fn().mockResolvedValue({ success: true }), cancelRequest: vi.fn().mockResolvedValue({ success: true }),
removeFriend: vi.fn().mockResolvedValue({ success: true }), removeFriend: vi.fn().mockResolvedValue({ success: true }),
search: vi.fn().mockResolvedValue([]), search: vi.fn().mockResolvedValue([]),
discover: vi.fn().mockResolvedValue({ users: [], total: 0 }),
}, },
}, },
})); }));
// Mock the instanceStore (imported by socialStore) // Mock the instanceStore (imported by socialStore)
vi.mock('../../stores/instanceStore', () => ({ vi.mock('../../stores/instanceStore', () => ({
useInstanceStore: { useInstanceStore: Object.assign(
getState: () => ({ instances: [] }), (selector: (s: any) => any) => selector({
setState: vi.fn(), instances: [],
subscribe: vi.fn(), _autoConnectDone: true,
}, }),
{
getState: () => ({ instances: [], _autoConnectDone: true }),
setState: vi.fn(),
subscribe: vi.fn(),
}
),
}));
vi.mock('../../stores/discoverStore', () => ({
useDiscoverStore: Object.assign(
(selector: (s: any) => any) => selector({
users: [],
isLoading: false,
searchQuery: '',
setSearchQuery: vi.fn(),
fetchUsers: vi.fn(),
updateRelationship: vi.fn(),
}),
{
getState: () => ({
users: [],
isLoading: false,
searchQuery: '',
fetchUsers: vi.fn(),
updateRelationship: vi.fn(),
}),
setState: vi.fn(),
subscribe: vi.fn(),
}
),
}));
vi.mock('../../stores/authStore', () => ({
useAuthStore: Object.assign(
(selector: (s: any) => any) => selector({
user: { id: 'current-user' },
}),
{
getState: () => ({ user: { id: 'current-user' } }),
setState: vi.fn(),
subscribe: vi.fn(),
}
),
}));
// Mock activityStore
vi.mock('../../stores/activityStore', () => ({
useActivityStore: Object.assign(
(selector: (s: any) => any) => selector({
userActivities: new Map(),
}),
{
getState: () => ({ userActivities: new Map(), reset: vi.fn() }),
setState: vi.fn(),
subscribe: vi.fn(),
}
),
}));
// Mock ConnectInstanceModal
vi.mock('../modals/ConnectInstanceModal', () => ({
ConnectInstanceModal: () => null,
})); }));
const mockNavigate = vi.fn(); const mockNavigate = vi.fn();
@@ -122,72 +186,103 @@ beforeEach(() => {
error: null, error: null,
loadFriends: vi.fn(), loadFriends: vi.fn(),
loadRequests: vi.fn(), loadRequests: vi.fn(),
searchUsers: vi.fn().mockResolvedValue([]),
}); });
useSpaceStore.setState({ useSpaceStore.setState({
dmChannels: [], dmChannels: [],
findExistingDmForUser: () => null,
}); });
}); });
describe('FriendsPage', () => { describe('FriendsPage', () => {
describe('Add Friend tab', () => { describe('Add Friend tab', () => {
it('renders the Add Friend form when tab is clicked', async () => { it('renders the search input when Add Friend tab is clicked', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
renderFriendsPage(); renderFriendsPage();
const addFriendTab = screen.getByText('Add Friend'); const addFriendTab = screen.getByText('Add Friend');
await user.click(addFriendTab); await user.click(addFriendTab);
expect(screen.getByPlaceholderText('You can add a friend with their username')).toBeInTheDocument(); expect(screen.getByPlaceholderText(/Search or add by username/)).toBeInTheDocument();
expect(screen.getByText('Send Friend Request')).toBeInTheDocument(); expect(screen.getByText('Find People')).toBeInTheDocument();
}); });
it('calls sendFriendRequest with the username when form is submitted', async () => { it('shows Direct Add row and sends request for user@domain input', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const mockSendFriendRequest = vi.fn().mockResolvedValue(undefined); const mockSendFriendRequest = vi.fn().mockResolvedValue('req-123');
useSocialStore.setState({ useSocialStore.setState({
sendFriendRequest: mockSendFriendRequest, sendFriendRequest: mockSendFriendRequest,
}); });
renderFriendsPage(); renderFriendsPage();
// Switch to Add Friend tab
await user.click(screen.getByText('Add Friend')); await user.click(screen.getByText('Add Friend'));
// Type username const input = screen.getByPlaceholderText(/Search or add by username/);
const input = screen.getByPlaceholderText('You can add a friend with their username'); await user.type(input, 'newbuddy@remote.example.com');
await user.type(input, 'newbuddy');
// Click send // Direct Add row should appear
await user.click(screen.getByText('Send Friend Request')); expect(screen.getByText(/Send friend request to/)).toBeInTheDocument();
// Click Send Request
await user.click(screen.getByText('Send Request'));
await waitFor(() => { await waitFor(() => {
expect(mockSendFriendRequest).toHaveBeenCalledWith('newbuddy'); expect(mockSendFriendRequest).toHaveBeenCalledWith('newbuddy@remote.example.com');
});
// Should show success message
await waitFor(() => {
expect(screen.getByText(/Success! Your friend request to newbuddy has been sent/)).toBeInTheDocument();
}); });
}); });
it('shows error when sendFriendRequest fails', async () => { it('shows toast when Direct Add request fails', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const mockSendFriendRequest = vi.fn().mockRejectedValue(new Error('User not found')); const mockSendFriendRequest = vi.fn().mockRejectedValue(new Error('User not found'));
const mockAddToast = vi.fn();
useSocialStore.setState({ useSocialStore.setState({
sendFriendRequest: mockSendFriendRequest, sendFriendRequest: mockSendFriendRequest,
}); });
useUIStore.setState({
addToast: mockAddToast,
});
renderFriendsPage(); renderFriendsPage();
await user.click(screen.getByText('Add Friend')); await user.click(screen.getByText('Add Friend'));
const input = screen.getByPlaceholderText('You can add a friend with their username'); const input = screen.getByPlaceholderText(/Search or add by username/);
await user.type(input, 'ghost'); await user.type(input, 'ghost@remote.example.com');
await user.click(screen.getByText('Send Friend Request')); await user.click(screen.getByText('Send Request'));
await waitFor(() => { await waitFor(() => {
expect(screen.getByText('User not found')).toBeInTheDocument(); expect(mockAddToast).toHaveBeenCalledWith('User not found', 'warning');
}); });
}); });
it('does not show Direct Add row for plain usernames', async () => {
const user = userEvent.setup();
renderFriendsPage();
await user.click(screen.getByText('Add Friend'));
const input = screen.getByPlaceholderText(/Search or add by username/);
await user.type(input, 'marc');
expect(screen.queryByText(/Send friend request to/)).not.toBeInTheDocument();
});
it('calls searchUsers when typing a non-@ query', async () => {
const user = userEvent.setup();
const mockSearchUsers = vi.fn().mockResolvedValue([]);
useSocialStore.setState({
searchUsers: mockSearchUsers,
});
renderFriendsPage();
await user.click(screen.getByText('Add Friend'));
const input = screen.getByPlaceholderText(/Search or add by username/);
await user.type(input, 'marc');
// Wait for debounce
await waitFor(() => {
expect(mockSearchUsers).toHaveBeenCalledWith('marc');
}, { timeout: 500 });
});
}); });
describe('DM button on friend item', () => { describe('DM button on friend item', () => {
@@ -202,6 +297,7 @@ describe('FriendsPage', () => {
}); });
useSpaceStore.setState({ useSpaceStore.setState({
addDmChannel: mockAddDmChannel, addDmChannel: mockAddDmChannel,
findExistingDmForUser: () => null,
}); });
// Mock the dm.create API // Mock the dm.create API
@@ -226,7 +322,7 @@ describe('FriendsPage', () => {
}); });
await waitFor(() => { await waitFor(() => {
expect(mockAddDmChannel).toHaveBeenCalledWith(expect.objectContaining({ id: 'dm-channel-99' })); expect(mockAddDmChannel).toHaveBeenCalledWith(expect.objectContaining({ id: 'dm-channel-99' }), '');
}); });
await waitFor(() => { await waitFor(() => {