feat: dashboard
This commit is contained in:
243
toju-app/src/app/features/dashboard/dashboard.component.spec.ts
Normal file
243
toju-app/src/app/features/dashboard/dashboard.component.spec.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
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<ServerInfo[]>(options.searchResults ?? []);
|
||||
const savedSig = signal<Room[]>(options.saved ?? []);
|
||||
const usersSig = signal<User[]>(options.users ?? []);
|
||||
const currentUserSig = signal<User | null>(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<string>(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']);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user