import '@angular/compiler'; import { describe, it, expect, vi } from 'vitest'; import { Injector, runInInjectionContext, signal } from '@angular/core'; import { Router } from '@angular/router'; import { Store } from '@ngrx/store'; import { of } from 'rxjs'; import { DashboardComponent } from './dashboard.component'; import { ViewportService } from '../../core/platform'; import { ServerDirectoryFacade } from '../../domains/server-directory/application/facades/server-directory.facade'; import { FriendService } from '../../domains/direct-message/application/services/friend.service'; import { RoomsActions } from '../../store/rooms/rooms.actions'; import { selectSearchResults, selectIsSearching, selectSavedRooms } from '../../store/rooms/rooms.selectors'; import { selectAllUsers, selectCurrentUser } from '../../store/users/users.selectors'; import type { ServerInfo } from '../../domains/server-directory/domain/models/server-directory.model'; import type { Room, User } from '../../shared-kernel'; interface HarnessOptions { searchResults?: ServerInfo[]; saved?: Room[]; users?: User[]; currentUser?: User | null; featured?: ServerInfo[]; trending?: ServerInfo[]; friendIds?: string[]; isMobile?: boolean; } function makeServer(id: string, name = id): ServerInfo { return { id, name, maxUsers: 50, userCount: 1, isPrivate: false } as unknown as ServerInfo; } function createHarness(options: HarnessOptions = {}) { const dispatch = vi.fn(); const searchResultsSig = signal(options.searchResults ?? []); const savedSig = signal(options.saved ?? []); const usersSig = signal(options.users ?? []); const currentUserSig = signal(options.currentUser ?? null); const store = { selectSignal: (selector: unknown) => { if (selector === selectSearchResults) { return searchResultsSig; } if (selector === selectIsSearching) { return signal(false); } if (selector === selectSavedRooms) { return savedSig; } if (selector === selectAllUsers) { return usersSig; } if (selector === selectCurrentUser) { return currentUserSig; } return signal(null); }, dispatch } as unknown as Store; const router = { navigate: vi.fn() } as unknown as Router; const getFeaturedServers = vi.fn(() => of(options.featured ?? [])); const getTrendingServers = vi.fn(() => of(options.trending ?? [])); const serverDirectory = { getFeaturedServers, getTrendingServers } as unknown as ServerDirectoryFacade; const friendIds = new Set(options.friendIds ?? []); const friendService = { friendIds: () => friendIds, friends: () => [] } as unknown as FriendService; const injector = Injector.create({ providers: [ DashboardComponent, { provide: Store, useValue: store }, { provide: Router, useValue: router }, { provide: ServerDirectoryFacade, useValue: serverDirectory }, { provide: FriendService, useValue: friendService }, { provide: ViewportService, useValue: { isMobile: signal(options.isMobile ?? false) } } ] }); const component = runInInjectionContext(injector, () => injector.get(DashboardComponent)); return { component, dispatch, router, getFeaturedServers, getTrendingServers }; } describe('DashboardComponent', () => { it('exposes the mobile viewport flag', () => { expect(createHarness().component.isMobile()).toBe(false); expect(createHarness({ isMobile: true }).component.isMobile()).toBe(true); }); it('reports a new user when there are no servers or users', () => { const { component } = createHarness(); expect(component.isNewUser()).toBe(true); }); it('filters people by the active query', () => { const { component } = createHarness({ users: [{ id: 'u1', displayName: 'Alice' } as unknown as User, { id: 'u2', displayName: 'Bob' } as unknown as User] }); component.onSearchChange('ali'); expect(component.topPeopleResults().map((user) => user.id)).toEqual(['u1']); }); it('limits server quick results', () => { const { component } = createHarness({ searchResults: Array.from({ length: 8 }, (_, index) => makeServer(`s${index}`)) }); component.onSearchChange('s'); expect(component.topServerResults()).toHaveLength(5); }); it('exposes an invite result for invite-like queries', () => { const { component } = createHarness(); component.onSearchChange('https://app.test/invite/Code_42'); expect(component.inviteResult()).toBe('Code_42'); }); it('opens a joined server in place and routes others to the servers page', () => { const joined = { id: 's1', name: 'Joined' } as unknown as Room; const { component, dispatch, router } = createHarness({ saved: [joined] }); component.openServer(makeServer('s1', 'Joined')); expect(dispatch).toHaveBeenCalledWith(RoomsActions.viewServer({ room: joined })); component.openServer(makeServer('s2', 'Other')); expect(router.navigate).toHaveBeenCalledWith(['/servers']); }); it('navigates to the invite route when opening an invite', () => { const { component, router } = createHarness(); component.onSearchChange('abc123'); component.openInvite(); expect(router.navigate).toHaveBeenCalledWith(['/invite', 'abc123']); }); it('suggests people you might know independent of the query, excluding self', () => { const { component } = createHarness({ users: [{ id: 'u1', oderId: 'u1', displayName: 'Alice' } as unknown as User, { id: 'u2', oderId: 'u2', displayName: 'Bob' } as unknown as User], currentUser: { id: 'u1', oderId: 'u1' } as unknown as User }); expect(component.peopleYouMightKnow().map((user) => user.id)).toEqual(['u2']); }); it('loads popular servers from featured results on init', () => { const featured = [makeServer('f1'), makeServer('f2')]; const { component } = createHarness({ featured }); component.ngOnInit(); expect(component.popularServers().map((server) => server.id)).toEqual(['f1', 'f2']); }); it('falls back to trending servers when featured is empty', () => { const trending = [makeServer('t1')]; const { component } = createHarness({ featured: [], trending }); component.ngOnInit(); expect(component.popularServers().map((server) => server.id)).toEqual(['t1']); }); it('limits recently active servers to the discovery cap', () => { const saved = Array.from({ length: 9 }, (_, index) => ({ id: `r${index}`, name: `Room ${index}`, userCount: 1 }) as unknown as Room); const { component } = createHarness({ saved }); expect(component.recentlyActiveServers()).toHaveLength(5); }); it('excludes existing friends from people you might know and lists them under friends', () => { const { component } = createHarness({ users: [ { id: 'u1', oderId: 'u1', displayName: 'Alice' } as unknown as User, { id: 'u2', oderId: 'u2', displayName: 'Bob' } as unknown as User, { id: 'u3', oderId: 'u3', displayName: 'Cara' } as unknown as User ], currentUser: { id: 'u1', oderId: 'u1' } as unknown as User, friendIds: ['u2'] }); expect(component.peopleYouMightKnow().map((user) => user.id)).toEqual(['u3']); expect(component.friends().map((user) => user.id)).toEqual(['u2']); }); it('records, removes, and clears recent searches', () => { const { component } = createHarness(); component.onSearchChange('gaming'); component.submitSearch(); component.onSearchChange('music'); component.submitSearch(); expect(component.recentSearches()).toEqual(['music', 'gaming']); component.removeRecentSearch('gaming'); expect(component.recentSearches()).toEqual(['music']); component.clearRecentSearches(); expect(component.recentSearches()).toEqual([]); }); it('deduplicates recent searches and keeps the most recent first', () => { const { component } = createHarness(); component.onSearchChange('gaming'); component.submitSearch(); component.onSearchChange('music'); component.submitSearch(); component.onSearchChange('gaming'); component.submitSearch(); expect(component.recentSearches()).toEqual(['gaming', 'music']); }); });