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