Barazo default frontend barazo.forum
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 203 lines 5.5 kB view raw
1/** 2 * Tests for notifications page. 3 */ 4 5import { describe, it, expect, vi, beforeEach } from 'vitest' 6import { render, screen, act, cleanup } from '@testing-library/react' 7import userEvent from '@testing-library/user-event' 8import { axe } from 'vitest-axe' 9import NotificationsPage from './page' 10 11// Mock next/navigation 12vi.mock('next/navigation', () => ({ 13 useRouter: () => ({ push: vi.fn(), replace: vi.fn() }), 14 usePathname: () => '/notifications', 15})) 16 17// Mock next-themes 18vi.mock('next-themes', () => ({ 19 useTheme: () => ({ theme: 'dark', setTheme: vi.fn() }), 20 ThemeProvider: ({ children }: { children: React.ReactNode }) => children, 21})) 22 23// Mock next/image 24vi.mock('next/image', () => ({ 25 default: (props: Record<string, unknown>) => { 26 // eslint-disable-next-line @next/next/no-img-element, jsx-a11y/alt-text 27 return <img {...props} /> 28 }, 29})) 30 31// Mock next/link 32vi.mock('next/link', () => ({ 33 default: ({ 34 children, 35 href, 36 ...props 37 }: { children: React.ReactNode; href: string } & Record<string, unknown>) => ( 38 <a href={href} {...props}> 39 {children} 40 </a> 41 ), 42})) 43 44// Mock API client 45vi.mock('@/lib/api/client', () => ({ 46 getNotifications: vi.fn(), 47 markNotificationsRead: vi.fn(), 48 getPublicSettings: vi.fn().mockResolvedValue({ 49 communityDid: 'did:plc:test-community-123', 50 communityName: 'Test Community', 51 maturityRating: 'safe', 52 communityDescription: null, 53 communityLogoUrl: null, 54 }), 55})) 56 57vi.mock('@/hooks/use-auth', () => { 58 const mockAuth = { 59 user: { 60 did: 'did:plc:user-jay-001', 61 handle: 'jay.bsky.team', 62 displayName: 'Jay', 63 avatarUrl: null, 64 }, 65 isAuthenticated: true, 66 isLoading: false, 67 getAccessToken: () => 'mock-access-token', 68 login: vi.fn(), 69 logout: vi.fn(), 70 setSessionFromCallback: vi.fn(), 71 authFetch: vi.fn(), 72 } 73 return { useAuth: () => mockAuth } 74}) 75 76import { getNotifications, markNotificationsRead } from '@/lib/api/client' 77 78const mockGetNotifications = vi.mocked(getNotifications) 79const mockMarkRead = vi.mocked(markNotificationsRead) 80 81const mockNotifications = [ 82 { 83 id: 'notif-1', 84 type: 'reply' as const, 85 userDid: 'did:plc:user', 86 actorDid: 'did:plc:alex', 87 actorHandle: 'alex.bsky.team', 88 subjectUri: 'at://did:plc:user/forum.barazo.topic.post/abc', 89 subjectTitle: 'My Topic', 90 subjectAuthorDid: 'did:plc:user', 91 subjectAuthorHandle: 'jay.bsky.team', 92 message: 'alex.bsky.team replied to your topic', 93 read: false, 94 createdAt: '2026-02-14T12:00:00Z', 95 }, 96 { 97 id: 'notif-2', 98 type: 'reaction' as const, 99 userDid: 'did:plc:user', 100 actorDid: 'did:plc:sam', 101 actorHandle: 'sam.example.com', 102 subjectUri: 'at://did:plc:user/forum.barazo.topic.post/abc', 103 subjectTitle: 'My Topic', 104 subjectAuthorDid: 'did:plc:user', 105 subjectAuthorHandle: 'jay.bsky.team', 106 message: 'sam.example.com reacted to your topic', 107 read: true, 108 createdAt: '2026-02-13T12:00:00Z', 109 }, 110] 111 112describe('NotificationsPage', () => { 113 beforeEach(() => { 114 vi.clearAllMocks() 115 cleanup() 116 mockGetNotifications.mockResolvedValue({ 117 notifications: mockNotifications, 118 cursor: null, 119 unreadCount: 1, 120 }) 121 mockMarkRead.mockResolvedValue(undefined) 122 }) 123 124 it('renders page heading', async () => { 125 render(<NotificationsPage />) 126 expect(screen.getByRole('heading', { level: 1, name: /notification/i })).toBeInTheDocument() 127 }) 128 129 it('displays notifications from API', async () => { 130 render(<NotificationsPage />) 131 132 await act(async () => { 133 await new Promise((r) => setTimeout(r, 100)) 134 }) 135 136 expect(await screen.findByText(/alex\.bsky\.team replied/)).toBeInTheDocument() 137 expect(screen.getByText(/sam\.example\.com reacted/)).toBeInTheDocument() 138 }) 139 140 it('shows unread indicator on unread notifications', async () => { 141 render(<NotificationsPage />) 142 143 await act(async () => { 144 await new Promise((r) => setTimeout(r, 100)) 145 }) 146 147 const items = screen.getAllByRole('article') 148 // First notification is unread 149 expect(items[0]).toHaveClass('border-l-primary') 150 }) 151 152 it('renders mark all read button', async () => { 153 render(<NotificationsPage />) 154 155 await act(async () => { 156 await new Promise((r) => setTimeout(r, 100)) 157 }) 158 159 expect(screen.getByRole('button', { name: /mark all read/i })).toBeInTheDocument() 160 }) 161 162 it('calls markNotificationsRead on mark all', async () => { 163 const user = userEvent.setup() 164 render(<NotificationsPage />) 165 166 await act(async () => { 167 await new Promise((r) => setTimeout(r, 100)) 168 }) 169 170 const button = screen.getByRole('button', { name: /mark all read/i }) 171 await user.click(button) 172 173 expect(mockMarkRead).toHaveBeenCalled() 174 }) 175 176 it('shows empty state when no notifications', async () => { 177 mockGetNotifications.mockResolvedValue({ 178 notifications: [], 179 cursor: null, 180 unreadCount: 0, 181 }) 182 183 render(<NotificationsPage />) 184 185 await act(async () => { 186 await new Promise((r) => setTimeout(r, 100)) 187 }) 188 189 expect(await screen.findByText(/no notifications/i)).toBeInTheDocument() 190 }) 191 192 it('renders breadcrumbs', () => { 193 render(<NotificationsPage />) 194 const nav = screen.getByRole('navigation', { name: /breadcrumb/i }) 195 expect(nav).toBeInTheDocument() 196 }) 197 198 it('passes axe accessibility check', async () => { 199 const { container } = render(<NotificationsPage />) 200 const results = await axe(container) 201 expect(results).toHaveNoViolations() 202 }) 203})