this repo has no description
1import { describe, it, expect, beforeEach } from 'vitest' 2import { render, screen, fireEvent, waitFor } from '@testing-library/svelte' 3import Notifications from '../routes/Notifications.svelte' 4import { 5 setupFetchMock, 6 mockEndpoint, 7 jsonResponse, 8 errorResponse, 9 mockData, 10 clearMocks, 11 setupAuthenticatedUser, 12 setupUnauthenticatedUser, 13} from './mocks' 14describe('Notifications', () => { 15 beforeEach(() => { 16 clearMocks() 17 setupFetchMock() 18 }) 19 describe('authentication guard', () => { 20 it('redirects to login when not authenticated', async () => { 21 setupUnauthenticatedUser() 22 render(Notifications) 23 await waitFor(() => { 24 expect(window.location.hash).toBe('#/login') 25 }) 26 }) 27 }) 28 describe('page structure', () => { 29 beforeEach(() => { 30 setupAuthenticatedUser() 31 mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 32 jsonResponse(mockData.notificationPrefs()) 33 ) 34 }) 35 it('displays all page elements and sections', async () => { 36 render(Notifications) 37 await waitFor(() => { 38 expect(screen.getByRole('heading', { name: /notification preferences/i, level: 1 })).toBeInTheDocument() 39 expect(screen.getByRole('link', { name: /dashboard/i })).toHaveAttribute('href', '#/dashboard') 40 expect(screen.getByText(/password resets/i)).toBeInTheDocument() 41 expect(screen.getByRole('heading', { name: /preferred channel/i })).toBeInTheDocument() 42 expect(screen.getByRole('heading', { name: /channel configuration/i })).toBeInTheDocument() 43 }) 44 }) 45 }) 46 describe('loading state', () => { 47 beforeEach(() => { 48 setupAuthenticatedUser() 49 }) 50 it('shows loading text while fetching preferences', async () => { 51 mockEndpoint('com.tranquil.account.getNotificationPrefs', async () => { 52 await new Promise(resolve => setTimeout(resolve, 100)) 53 return jsonResponse(mockData.notificationPrefs()) 54 }) 55 render(Notifications) 56 expect(screen.getByText(/loading/i)).toBeInTheDocument() 57 }) 58 }) 59 describe('channel options', () => { 60 beforeEach(() => { 61 setupAuthenticatedUser() 62 }) 63 it('displays all four channel options', async () => { 64 mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 65 jsonResponse(mockData.notificationPrefs()) 66 ) 67 render(Notifications) 68 await waitFor(() => { 69 expect(screen.getByRole('radio', { name: /email/i })).toBeInTheDocument() 70 expect(screen.getByRole('radio', { name: /discord/i })).toBeInTheDocument() 71 expect(screen.getByRole('radio', { name: /telegram/i })).toBeInTheDocument() 72 expect(screen.getByRole('radio', { name: /signal/i })).toBeInTheDocument() 73 }) 74 }) 75 it('email channel is always selectable', async () => { 76 mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 77 jsonResponse(mockData.notificationPrefs()) 78 ) 79 render(Notifications) 80 await waitFor(() => { 81 const emailRadio = screen.getByRole('radio', { name: /email/i }) 82 expect(emailRadio).not.toBeDisabled() 83 }) 84 }) 85 it('discord channel is disabled when not configured', async () => { 86 mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 87 jsonResponse(mockData.notificationPrefs({ discordId: null })) 88 ) 89 render(Notifications) 90 await waitFor(() => { 91 const discordRadio = screen.getByRole('radio', { name: /discord/i }) 92 expect(discordRadio).toBeDisabled() 93 }) 94 }) 95 it('discord channel is enabled when configured', async () => { 96 mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 97 jsonResponse(mockData.notificationPrefs({ discordId: '123456789' })) 98 ) 99 render(Notifications) 100 await waitFor(() => { 101 const discordRadio = screen.getByRole('radio', { name: /discord/i }) 102 expect(discordRadio).not.toBeDisabled() 103 }) 104 }) 105 it('shows hint for disabled channels', async () => { 106 mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 107 jsonResponse(mockData.notificationPrefs()) 108 ) 109 render(Notifications) 110 await waitFor(() => { 111 expect(screen.getAllByText(/configure below to enable/i).length).toBeGreaterThan(0) 112 }) 113 }) 114 it('selects current preferred channel', async () => { 115 mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 116 jsonResponse(mockData.notificationPrefs({ preferredChannel: 'email' })) 117 ) 118 render(Notifications) 119 await waitFor(() => { 120 const emailRadio = screen.getByRole('radio', { name: /email/i }) as HTMLInputElement 121 expect(emailRadio.checked).toBe(true) 122 }) 123 }) 124 }) 125 describe('channel configuration', () => { 126 beforeEach(() => { 127 setupAuthenticatedUser() 128 }) 129 it('displays email as readonly with current value', async () => { 130 mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 131 jsonResponse(mockData.notificationPrefs()) 132 ) 133 render(Notifications) 134 await waitFor(() => { 135 const emailInput = screen.getByLabelText(/^email$/i) as HTMLInputElement 136 expect(emailInput).toBeDisabled() 137 expect(emailInput.value).toBe('test@example.com') 138 }) 139 }) 140 it('displays all channel inputs with current values', async () => { 141 mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 142 jsonResponse(mockData.notificationPrefs({ 143 discordId: '123456789', 144 telegramUsername: 'testuser', 145 signalNumber: '+1234567890', 146 })) 147 ) 148 render(Notifications) 149 await waitFor(() => { 150 expect((screen.getByLabelText(/discord user id/i) as HTMLInputElement).value).toBe('123456789') 151 expect((screen.getByLabelText(/telegram username/i) as HTMLInputElement).value).toBe('testuser') 152 expect((screen.getByLabelText(/signal phone number/i) as HTMLInputElement).value).toBe('+1234567890') 153 }) 154 }) 155 }) 156 describe('verification status badges', () => { 157 beforeEach(() => { 158 setupAuthenticatedUser() 159 }) 160 it('shows Primary badge for email', async () => { 161 mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 162 jsonResponse(mockData.notificationPrefs()) 163 ) 164 render(Notifications) 165 await waitFor(() => { 166 expect(screen.getByText('Primary')).toBeInTheDocument() 167 }) 168 }) 169 it('shows Verified badge for verified discord', async () => { 170 mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 171 jsonResponse(mockData.notificationPrefs({ 172 discordId: '123456789', 173 discordVerified: true, 174 })) 175 ) 176 render(Notifications) 177 await waitFor(() => { 178 const verifiedBadges = screen.getAllByText('Verified') 179 expect(verifiedBadges.length).toBeGreaterThan(0) 180 }) 181 }) 182 it('shows Not verified badge for unverified discord', async () => { 183 mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 184 jsonResponse(mockData.notificationPrefs({ 185 discordId: '123456789', 186 discordVerified: false, 187 })) 188 ) 189 render(Notifications) 190 await waitFor(() => { 191 expect(screen.getByText('Not verified')).toBeInTheDocument() 192 }) 193 }) 194 it('does not show badge when channel not configured', async () => { 195 mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 196 jsonResponse(mockData.notificationPrefs()) 197 ) 198 render(Notifications) 199 await waitFor(() => { 200 expect(screen.getByText('Primary')).toBeInTheDocument() 201 expect(screen.queryByText('Not verified')).not.toBeInTheDocument() 202 }) 203 }) 204 }) 205 describe('save preferences', () => { 206 beforeEach(() => { 207 setupAuthenticatedUser() 208 }) 209 it('calls updateNotificationPrefs with correct data', async () => { 210 let capturedBody: Record<string, unknown> | null = null 211 mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 212 jsonResponse(mockData.notificationPrefs()) 213 ) 214 mockEndpoint('com.tranquil.account.updateNotificationPrefs', (_url, options) => { 215 capturedBody = JSON.parse((options?.body as string) || '{}') 216 return jsonResponse({ success: true }) 217 }) 218 render(Notifications) 219 await waitFor(() => { 220 expect(screen.getByLabelText(/discord user id/i)).toBeInTheDocument() 221 }) 222 await fireEvent.input(screen.getByLabelText(/discord user id/i), { target: { value: '999888777' } }) 223 await fireEvent.click(screen.getByRole('button', { name: /save preferences/i })) 224 await waitFor(() => { 225 expect(capturedBody).not.toBeNull() 226 expect(capturedBody?.discordId).toBe('999888777') 227 expect(capturedBody?.preferredChannel).toBe('email') 228 }) 229 }) 230 it('shows loading state while saving', async () => { 231 mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 232 jsonResponse(mockData.notificationPrefs()) 233 ) 234 mockEndpoint('com.tranquil.account.updateNotificationPrefs', async () => { 235 await new Promise(resolve => setTimeout(resolve, 100)) 236 return jsonResponse({ success: true }) 237 }) 238 render(Notifications) 239 await waitFor(() => { 240 expect(screen.getByRole('button', { name: /save preferences/i })).toBeInTheDocument() 241 }) 242 await fireEvent.click(screen.getByRole('button', { name: /save preferences/i })) 243 expect(screen.getByRole('button', { name: /saving/i })).toBeInTheDocument() 244 expect(screen.getByRole('button', { name: /saving/i })).toBeDisabled() 245 }) 246 it('shows success message after saving', async () => { 247 mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 248 jsonResponse(mockData.notificationPrefs()) 249 ) 250 mockEndpoint('com.tranquil.account.updateNotificationPrefs', () => 251 jsonResponse({ success: true }) 252 ) 253 render(Notifications) 254 await waitFor(() => { 255 expect(screen.getByRole('button', { name: /save preferences/i })).toBeInTheDocument() 256 }) 257 await fireEvent.click(screen.getByRole('button', { name: /save preferences/i })) 258 await waitFor(() => { 259 expect(screen.getByText(/notification preferences saved/i)).toBeInTheDocument() 260 }) 261 }) 262 it('shows error when save fails', async () => { 263 mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 264 jsonResponse(mockData.notificationPrefs()) 265 ) 266 mockEndpoint('com.tranquil.account.updateNotificationPrefs', () => 267 errorResponse('InvalidRequest', 'Invalid channel configuration', 400) 268 ) 269 render(Notifications) 270 await waitFor(() => { 271 expect(screen.getByRole('button', { name: /save preferences/i })).toBeInTheDocument() 272 }) 273 await fireEvent.click(screen.getByRole('button', { name: /save preferences/i })) 274 await waitFor(() => { 275 expect(screen.getByText(/invalid channel configuration/i)).toBeInTheDocument() 276 expect(screen.getByText(/invalid channel configuration/i).closest('.message')).toHaveClass('error') 277 }) 278 }) 279 it('reloads preferences after successful save', async () => { 280 let loadCount = 0 281 mockEndpoint('com.tranquil.account.getNotificationPrefs', () => { 282 loadCount++ 283 return jsonResponse(mockData.notificationPrefs()) 284 }) 285 mockEndpoint('com.tranquil.account.updateNotificationPrefs', () => 286 jsonResponse({ success: true }) 287 ) 288 render(Notifications) 289 await waitFor(() => { 290 expect(screen.getByRole('button', { name: /save preferences/i })).toBeInTheDocument() 291 }) 292 const initialLoadCount = loadCount 293 await fireEvent.click(screen.getByRole('button', { name: /save preferences/i })) 294 await waitFor(() => { 295 expect(loadCount).toBeGreaterThan(initialLoadCount) 296 }) 297 }) 298 }) 299 describe('channel selection interaction', () => { 300 beforeEach(() => { 301 setupAuthenticatedUser() 302 }) 303 it('enables discord channel after entering discord ID', async () => { 304 mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 305 jsonResponse(mockData.notificationPrefs()) 306 ) 307 render(Notifications) 308 await waitFor(() => { 309 expect(screen.getByRole('radio', { name: /discord/i })).toBeDisabled() 310 }) 311 await fireEvent.input(screen.getByLabelText(/discord user id/i), { target: { value: '123456789' } }) 312 await waitFor(() => { 313 expect(screen.getByRole('radio', { name: /discord/i })).not.toBeDisabled() 314 }) 315 }) 316 it('allows selecting a configured channel', async () => { 317 mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 318 jsonResponse(mockData.notificationPrefs({ 319 discordId: '123456789', 320 discordVerified: true, 321 })) 322 ) 323 render(Notifications) 324 await waitFor(() => { 325 expect(screen.getByRole('radio', { name: /discord/i })).not.toBeDisabled() 326 }) 327 await fireEvent.click(screen.getByRole('radio', { name: /discord/i })) 328 const discordRadio = screen.getByRole('radio', { name: /discord/i }) as HTMLInputElement 329 expect(discordRadio.checked).toBe(true) 330 }) 331 }) 332 describe('error handling', () => { 333 beforeEach(() => { 334 setupAuthenticatedUser() 335 }) 336 it('shows error when loading preferences fails', async () => { 337 mockEndpoint('com.tranquil.account.getNotificationPrefs', () => 338 errorResponse('InternalError', 'Database connection failed', 500) 339 ) 340 render(Notifications) 341 await waitFor(() => { 342 expect(screen.getByText(/database connection failed/i)).toBeInTheDocument() 343 }) 344 }) 345 }) 346})