Sifa professional network frontend (Next.js, React, TailwindCSS) sifa.id/
at main 142 lines 5.7 kB view raw
1import { render, screen } from '@testing-library/react'; 2import userEvent from '@testing-library/user-event'; 3import { describe, expect, it, vi } from 'vitest'; 4 5import type { ActiveApp } from '@/lib/types'; 6 7vi.mock('next-intl', () => ({ 8 useTranslations: () => (key: string, values?: Record<string, unknown>) => { 9 if (key === 'activeOn') return `Active on ${values?.app}`; 10 if (key === 'moreApps') return `+${values?.count} more`; 11 if (key === 'label') return 'Active apps'; 12 return key; 13 }, 14})); 15 16const mockApps: ActiveApp[] = [ 17 { id: 'bluesky', name: 'Bluesky network', category: 'social', recentCount: 42 }, 18 { id: 'tangled', name: 'Tangled', category: 'dev', recentCount: 15 }, 19 { id: 'whitewind', name: 'Whitewind', category: 'blog', recentCount: 8 }, 20]; 21 22const manyApps: ActiveApp[] = [ 23 { id: 'bluesky', name: 'Bluesky network', category: 'social', recentCount: 42 }, 24 { id: 'tangled', name: 'Tangled', category: 'dev', recentCount: 15 }, 25 { id: 'whitewind', name: 'Whitewind', category: 'blog', recentCount: 8 }, 26 { id: 'frontpage', name: 'Frontpage', category: 'social', recentCount: 5 }, 27 { id: 'smokesignal', name: 'Events', category: 'events', recentCount: 3 }, 28 { id: 'picosky', name: 'Picosky', category: 'social', recentCount: 2 }, 29 { id: 'linkat', name: 'Linkat', category: 'links', recentCount: 1 }, 30]; 31 32describe('ActivityIndicators', () => { 33 // Lazy import so the mock is in place before module loads 34 async function loadComponent() { 35 const mod = await import('./activity-indicators'); 36 return mod.ActivityIndicators; 37 } 38 39 it('renders pills for each active app', { timeout: 60_000 }, async () => { 40 const ActivityIndicators = await loadComponent(); 41 render(<ActivityIndicators apps={mockApps} />); 42 43 expect(screen.getByText('Bluesky network')).toBeDefined(); 44 expect(screen.getByText('Tangled')).toBeDefined(); 45 expect(screen.getByText('Whitewind')).toBeDefined(); 46 }); 47 48 it('renders nothing when apps is empty', async () => { 49 const ActivityIndicators = await loadComponent(); 50 const { container } = render(<ActivityIndicators apps={[]} />); 51 52 expect(container.innerHTML).toBe(''); 53 }); 54 55 it('does not show counts in pills', async () => { 56 const ActivityIndicators = await loadComponent(); 57 render(<ActivityIndicators apps={mockApps} />); 58 59 expect(screen.queryByText('42')).toBeNull(); 60 expect(screen.queryByText('15')).toBeNull(); 61 expect(screen.queryByText('8')).toBeNull(); 62 }); 63 64 it('has accessible labels on pills', async () => { 65 const ActivityIndicators = await loadComponent(); 66 render(<ActivityIndicators apps={mockApps} />); 67 68 expect(screen.getByLabelText('Active on Bluesky network')).toBeDefined(); 69 expect(screen.getByLabelText('Active on Tangled')).toBeDefined(); 70 expect(screen.getByLabelText('Active on Whitewind')).toBeDefined(); 71 }); 72 73 it('shows overflow button when more than maxVisible apps', async () => { 74 const ActivityIndicators = await loadComponent(); 75 render(<ActivityIndicators apps={manyApps} maxVisible={5} />); 76 77 const moreButton = screen.getByRole('button', { name: /\+2 more/i }); 78 expect(moreButton).toBeDefined(); 79 expect(moreButton.getAttribute('aria-expanded')).toBe('false'); 80 }); 81 82 it('expands overflow on click', async () => { 83 const user = userEvent.setup(); 84 const ActivityIndicators = await loadComponent(); 85 render(<ActivityIndicators apps={manyApps} maxVisible={5} />); 86 87 // Before expand: overflow apps should not be visible 88 expect(screen.queryByText('Picosky')).toBeNull(); 89 expect(screen.queryByText('Linkat')).toBeNull(); 90 91 const moreButton = screen.getByRole('button', { name: /\+2 more/i }); 92 await user.click(moreButton); 93 94 // After expand: all apps visible 95 expect(screen.getByText('Picosky')).toBeDefined(); 96 expect(screen.getByText('Linkat')).toBeDefined(); 97 }); 98 99 it('calls onFilter when a pill is clicked', async () => { 100 const user = userEvent.setup(); 101 const onFilter = vi.fn(); 102 const ActivityIndicators = await loadComponent(); 103 render(<ActivityIndicators apps={mockApps} onFilter={onFilter} />); 104 105 await user.click(screen.getByLabelText('Active on Bluesky network')); 106 expect(onFilter).toHaveBeenCalledWith('bluesky'); 107 }); 108 109 it('toggles filter off when active pill is clicked again', async () => { 110 const user = userEvent.setup(); 111 const onFilter = vi.fn(); 112 const ActivityIndicators = await loadComponent(); 113 render(<ActivityIndicators apps={mockApps} activeFilter="bluesky" onFilter={onFilter} />); 114 115 await user.click(screen.getByLabelText('Active on Bluesky network')); 116 expect(onFilter).toHaveBeenCalledWith(null); 117 }); 118 119 it('sets aria-pressed on active filter pill', async () => { 120 const onFilter = vi.fn(); 121 const ActivityIndicators = await loadComponent(); 122 render(<ActivityIndicators apps={mockApps} activeFilter="bluesky" onFilter={onFilter} />); 123 124 const blueskyPill = screen.getByLabelText('Active on Bluesky network'); 125 expect(blueskyPill.getAttribute('aria-pressed')).toBe('true'); 126 127 const tangledPill = screen.getByLabelText('Active on Tangled'); 128 expect(tangledPill.getAttribute('aria-pressed')).toBe('false'); 129 }); 130 131 it('renders spans (not buttons) when onFilter is not provided', async () => { 132 const ActivityIndicators = await loadComponent(); 133 render(<ActivityIndicators apps={mockApps} />); 134 135 const blueskyPill = screen.getByLabelText('Active on Bluesky network'); 136 expect(blueskyPill.tagName).toBe('SPAN'); 137 138 // No buttons for pills (only possible overflow button if needed) 139 const buttons = screen.queryAllByRole('button'); 140 expect(buttons).toHaveLength(0); 141 }); 142});