this repo has no description
1import { describe, it, expect, beforeEach, vi } from 'vitest' 2import { render, screen, fireEvent, waitFor } from '@testing-library/svelte' 3import Settings from '../routes/Settings.svelte' 4import { 5 setupFetchMock, 6 mockEndpoint, 7 jsonResponse, 8 errorResponse, 9 clearMocks, 10 setupAuthenticatedUser, 11 setupUnauthenticatedUser, 12} from './mocks' 13 14describe('Settings', () => { 15 beforeEach(() => { 16 clearMocks() 17 setupFetchMock() 18 window.confirm = vi.fn(() => true) 19 }) 20 21 describe('authentication guard', () => { 22 it('redirects to login when not authenticated', async () => { 23 setupUnauthenticatedUser() 24 render(Settings) 25 26 await waitFor(() => { 27 expect(window.location.hash).toBe('#/login') 28 }) 29 }) 30 }) 31 32 describe('page structure', () => { 33 beforeEach(() => { 34 setupAuthenticatedUser() 35 }) 36 37 it('displays all page elements and sections', async () => { 38 render(Settings) 39 40 await waitFor(() => { 41 expect(screen.getByRole('heading', { name: /account settings/i, level: 1 })).toBeInTheDocument() 42 expect(screen.getByRole('link', { name: /dashboard/i })).toHaveAttribute('href', '#/dashboard') 43 expect(screen.getByRole('heading', { name: /change email/i })).toBeInTheDocument() 44 expect(screen.getByRole('heading', { name: /change handle/i })).toBeInTheDocument() 45 expect(screen.getByRole('heading', { name: /delete account/i })).toBeInTheDocument() 46 }) 47 }) 48 }) 49 50 describe('email change', () => { 51 beforeEach(() => { 52 setupAuthenticatedUser() 53 }) 54 55 it('displays current email and input field', async () => { 56 render(Settings) 57 58 await waitFor(() => { 59 expect(screen.getByText(/current: test@example.com/i)).toBeInTheDocument() 60 expect(screen.getByLabelText(/new email/i)).toBeInTheDocument() 61 }) 62 }) 63 64 it('calls requestEmailUpdate when submitting', async () => { 65 let requestCalled = false 66 67 mockEndpoint('com.atproto.server.requestEmailUpdate', () => { 68 requestCalled = true 69 return jsonResponse({ tokenRequired: true }) 70 }) 71 72 render(Settings) 73 74 await waitFor(() => { 75 expect(screen.getByLabelText(/new email/i)).toBeInTheDocument() 76 }) 77 78 await fireEvent.input(screen.getByLabelText(/new email/i), { target: { value: 'newemail@example.com' } }) 79 await fireEvent.click(screen.getByRole('button', { name: /change email/i })) 80 81 await waitFor(() => { 82 expect(requestCalled).toBe(true) 83 }) 84 }) 85 86 it('shows verification code input when token is required', async () => { 87 mockEndpoint('com.atproto.server.requestEmailUpdate', () => 88 jsonResponse({ tokenRequired: true }) 89 ) 90 91 render(Settings) 92 93 await waitFor(() => { 94 expect(screen.getByLabelText(/new email/i)).toBeInTheDocument() 95 }) 96 97 await fireEvent.input(screen.getByLabelText(/new email/i), { target: { value: 'newemail@example.com' } }) 98 await fireEvent.click(screen.getByRole('button', { name: /change email/i })) 99 100 await waitFor(() => { 101 expect(screen.getByLabelText(/verification code/i)).toBeInTheDocument() 102 expect(screen.getByRole('button', { name: /confirm email change/i })).toBeInTheDocument() 103 }) 104 }) 105 106 it('calls updateEmail with token when confirming', async () => { 107 let updateCalled = false 108 let capturedBody: Record<string, string> | null = null 109 110 mockEndpoint('com.atproto.server.requestEmailUpdate', () => 111 jsonResponse({ tokenRequired: true }) 112 ) 113 114 mockEndpoint('com.atproto.server.updateEmail', (_url, options) => { 115 updateCalled = true 116 capturedBody = JSON.parse((options?.body as string) || '{}') 117 return jsonResponse({}) 118 }) 119 120 render(Settings) 121 122 await waitFor(() => { 123 expect(screen.getByLabelText(/new email/i)).toBeInTheDocument() 124 }) 125 126 await fireEvent.input(screen.getByLabelText(/new email/i), { target: { value: 'newemail@example.com' } }) 127 await fireEvent.click(screen.getByRole('button', { name: /change email/i })) 128 129 await waitFor(() => { 130 expect(screen.getByLabelText(/verification code/i)).toBeInTheDocument() 131 }) 132 133 await fireEvent.input(screen.getByLabelText(/verification code/i), { target: { value: '123456' } }) 134 await fireEvent.click(screen.getByRole('button', { name: /confirm email change/i })) 135 136 await waitFor(() => { 137 expect(updateCalled).toBe(true) 138 expect(capturedBody?.email).toBe('newemail@example.com') 139 expect(capturedBody?.token).toBe('123456') 140 }) 141 }) 142 143 it('shows success message after email update', async () => { 144 mockEndpoint('com.atproto.server.requestEmailUpdate', () => 145 jsonResponse({ tokenRequired: true }) 146 ) 147 148 mockEndpoint('com.atproto.server.updateEmail', () => 149 jsonResponse({}) 150 ) 151 152 render(Settings) 153 154 await waitFor(() => { 155 expect(screen.getByLabelText(/new email/i)).toBeInTheDocument() 156 }) 157 158 await fireEvent.input(screen.getByLabelText(/new email/i), { target: { value: 'new@test.com' } }) 159 await fireEvent.click(screen.getByRole('button', { name: /change email/i })) 160 161 await waitFor(() => { 162 expect(screen.getByLabelText(/verification code/i)).toBeInTheDocument() 163 }) 164 165 await fireEvent.input(screen.getByLabelText(/verification code/i), { target: { value: '123456' } }) 166 await fireEvent.click(screen.getByRole('button', { name: /confirm email change/i })) 167 168 await waitFor(() => { 169 expect(screen.getByText(/email updated successfully/i)).toBeInTheDocument() 170 }) 171 }) 172 173 it('shows cancel button to return to email form', async () => { 174 mockEndpoint('com.atproto.server.requestEmailUpdate', () => 175 jsonResponse({ tokenRequired: true }) 176 ) 177 178 render(Settings) 179 180 await waitFor(() => { 181 expect(screen.getByLabelText(/new email/i)).toBeInTheDocument() 182 }) 183 184 await fireEvent.input(screen.getByLabelText(/new email/i), { target: { value: 'new@test.com' } }) 185 await fireEvent.click(screen.getByRole('button', { name: /change email/i })) 186 187 await waitFor(() => { 188 expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument() 189 }) 190 191 await fireEvent.click(screen.getByRole('button', { name: /cancel/i })) 192 193 await waitFor(() => { 194 expect(screen.getByLabelText(/new email/i)).toBeInTheDocument() 195 expect(screen.queryByLabelText(/verification code/i)).not.toBeInTheDocument() 196 }) 197 }) 198 199 it('shows error when email update fails', async () => { 200 mockEndpoint('com.atproto.server.requestEmailUpdate', () => 201 errorResponse('InvalidEmail', 'Invalid email format', 400) 202 ) 203 204 render(Settings) 205 206 await waitFor(() => { 207 expect(screen.getByLabelText(/new email/i)).toBeInTheDocument() 208 }) 209 210 await fireEvent.input(screen.getByLabelText(/new email/i), { target: { value: 'invalid@test.com' } }) 211 212 await waitFor(() => { 213 expect(screen.getByRole('button', { name: /change email/i })).not.toBeDisabled() 214 }) 215 216 await fireEvent.click(screen.getByRole('button', { name: /change email/i })) 217 218 await waitFor(() => { 219 expect(screen.getByText(/invalid email format/i)).toBeInTheDocument() 220 }) 221 }) 222 }) 223 224 describe('handle change', () => { 225 beforeEach(() => { 226 setupAuthenticatedUser() 227 }) 228 229 it('displays current handle', async () => { 230 render(Settings) 231 232 await waitFor(() => { 233 expect(screen.getByText(/current: @testuser\.test\.bspds\.dev/i)).toBeInTheDocument() 234 }) 235 }) 236 237 it('calls updateHandle with new handle', async () => { 238 let capturedHandle: string | null = null 239 240 mockEndpoint('com.atproto.identity.updateHandle', (_url, options) => { 241 const body = JSON.parse((options?.body as string) || '{}') 242 capturedHandle = body.handle 243 return jsonResponse({}) 244 }) 245 246 render(Settings) 247 248 await waitFor(() => { 249 expect(screen.getByLabelText(/new handle/i)).toBeInTheDocument() 250 }) 251 252 await fireEvent.input(screen.getByLabelText(/new handle/i), { target: { value: 'newhandle.bsky.social' } }) 253 await fireEvent.click(screen.getByRole('button', { name: /change handle/i })) 254 255 await waitFor(() => { 256 expect(capturedHandle).toBe('newhandle.bsky.social') 257 }) 258 }) 259 260 it('shows success message after handle change', async () => { 261 mockEndpoint('com.atproto.identity.updateHandle', () => 262 jsonResponse({}) 263 ) 264 265 render(Settings) 266 267 await waitFor(() => { 268 expect(screen.getByLabelText(/new handle/i)).toBeInTheDocument() 269 }) 270 271 await fireEvent.input(screen.getByLabelText(/new handle/i), { target: { value: 'newhandle' } }) 272 await fireEvent.click(screen.getByRole('button', { name: /change handle/i })) 273 274 await waitFor(() => { 275 expect(screen.getByText(/handle updated successfully/i)).toBeInTheDocument() 276 }) 277 }) 278 279 it('shows error when handle change fails', async () => { 280 mockEndpoint('com.atproto.identity.updateHandle', () => 281 errorResponse('HandleNotAvailable', 'Handle is already taken', 400) 282 ) 283 284 render(Settings) 285 286 await waitFor(() => { 287 expect(screen.getByLabelText(/new handle/i)).toBeInTheDocument() 288 }) 289 290 await fireEvent.input(screen.getByLabelText(/new handle/i), { target: { value: 'taken' } }) 291 await fireEvent.click(screen.getByRole('button', { name: /change handle/i })) 292 293 await waitFor(() => { 294 expect(screen.getByText(/handle is already taken/i)).toBeInTheDocument() 295 }) 296 }) 297 }) 298 299 describe('account deletion', () => { 300 beforeEach(() => { 301 setupAuthenticatedUser() 302 mockEndpoint('com.atproto.server.deleteSession', () => 303 jsonResponse({}) 304 ) 305 }) 306 307 it('displays delete section with warning and request button', async () => { 308 render(Settings) 309 310 await waitFor(() => { 311 expect(screen.getByText(/this action is irreversible/i)).toBeInTheDocument() 312 expect(screen.getByRole('button', { name: /request account deletion/i })).toBeInTheDocument() 313 }) 314 }) 315 316 it('calls requestAccountDelete when clicking request', async () => { 317 let requestCalled = false 318 319 mockEndpoint('com.atproto.server.requestAccountDelete', () => { 320 requestCalled = true 321 return jsonResponse({}) 322 }) 323 324 render(Settings) 325 326 await waitFor(() => { 327 expect(screen.getByRole('button', { name: /request account deletion/i })).toBeInTheDocument() 328 }) 329 330 await fireEvent.click(screen.getByRole('button', { name: /request account deletion/i })) 331 332 await waitFor(() => { 333 expect(requestCalled).toBe(true) 334 }) 335 }) 336 337 it('shows confirmation form after requesting deletion', async () => { 338 mockEndpoint('com.atproto.server.requestAccountDelete', () => 339 jsonResponse({}) 340 ) 341 342 render(Settings) 343 344 await waitFor(() => { 345 expect(screen.getByRole('button', { name: /request account deletion/i })).toBeInTheDocument() 346 }) 347 348 await fireEvent.click(screen.getByRole('button', { name: /request account deletion/i })) 349 350 await waitFor(() => { 351 expect(screen.getByLabelText(/confirmation code/i)).toBeInTheDocument() 352 expect(screen.getByLabelText(/your password/i)).toBeInTheDocument() 353 expect(screen.getByRole('button', { name: /permanently delete account/i })).toBeInTheDocument() 354 }) 355 }) 356 357 it('shows confirmation dialog before final deletion', async () => { 358 const confirmSpy = vi.fn(() => false) 359 window.confirm = confirmSpy 360 361 mockEndpoint('com.atproto.server.requestAccountDelete', () => 362 jsonResponse({}) 363 ) 364 365 render(Settings) 366 367 await waitFor(() => { 368 expect(screen.getByRole('button', { name: /request account deletion/i })).toBeInTheDocument() 369 }) 370 371 await fireEvent.click(screen.getByRole('button', { name: /request account deletion/i })) 372 373 await waitFor(() => { 374 expect(screen.getByLabelText(/confirmation code/i)).toBeInTheDocument() 375 }) 376 377 await fireEvent.input(screen.getByLabelText(/confirmation code/i), { target: { value: 'ABC123' } }) 378 await fireEvent.input(screen.getByLabelText(/your password/i), { target: { value: 'password' } }) 379 await fireEvent.click(screen.getByRole('button', { name: /permanently delete account/i })) 380 381 expect(confirmSpy).toHaveBeenCalledWith( 382 expect.stringContaining('absolutely sure') 383 ) 384 }) 385 386 it('calls deleteAccount with correct parameters', async () => { 387 window.confirm = vi.fn(() => true) 388 let capturedBody: Record<string, string> | null = null 389 390 mockEndpoint('com.atproto.server.requestAccountDelete', () => 391 jsonResponse({}) 392 ) 393 394 mockEndpoint('com.atproto.server.deleteAccount', (_url, options) => { 395 capturedBody = JSON.parse((options?.body as string) || '{}') 396 return jsonResponse({}) 397 }) 398 399 render(Settings) 400 401 await waitFor(() => { 402 expect(screen.getByRole('button', { name: /request account deletion/i })).toBeInTheDocument() 403 }) 404 405 await fireEvent.click(screen.getByRole('button', { name: /request account deletion/i })) 406 407 await waitFor(() => { 408 expect(screen.getByLabelText(/confirmation code/i)).toBeInTheDocument() 409 }) 410 411 await fireEvent.input(screen.getByLabelText(/confirmation code/i), { target: { value: 'DEL123' } }) 412 await fireEvent.input(screen.getByLabelText(/your password/i), { target: { value: 'mypassword' } }) 413 await fireEvent.click(screen.getByRole('button', { name: /permanently delete account/i })) 414 415 await waitFor(() => { 416 expect(capturedBody?.token).toBe('DEL123') 417 expect(capturedBody?.password).toBe('mypassword') 418 expect(capturedBody?.did).toBe('did:web:test.bspds.dev:u:testuser') 419 }) 420 }) 421 422 it('navigates to login after successful deletion', async () => { 423 window.confirm = vi.fn(() => true) 424 425 mockEndpoint('com.atproto.server.requestAccountDelete', () => 426 jsonResponse({}) 427 ) 428 429 mockEndpoint('com.atproto.server.deleteAccount', () => 430 jsonResponse({}) 431 ) 432 433 render(Settings) 434 435 await waitFor(() => { 436 expect(screen.getByRole('button', { name: /request account deletion/i })).toBeInTheDocument() 437 }) 438 439 await fireEvent.click(screen.getByRole('button', { name: /request account deletion/i })) 440 441 await waitFor(() => { 442 expect(screen.getByLabelText(/confirmation code/i)).toBeInTheDocument() 443 }) 444 445 await fireEvent.input(screen.getByLabelText(/confirmation code/i), { target: { value: 'DEL123' } }) 446 await fireEvent.input(screen.getByLabelText(/your password/i), { target: { value: 'password' } }) 447 await fireEvent.click(screen.getByRole('button', { name: /permanently delete account/i })) 448 449 await waitFor(() => { 450 expect(window.location.hash).toBe('#/login') 451 }) 452 }) 453 454 it('shows cancel button to return to request state', async () => { 455 mockEndpoint('com.atproto.server.requestAccountDelete', () => 456 jsonResponse({}) 457 ) 458 459 render(Settings) 460 461 await waitFor(() => { 462 expect(screen.getByRole('button', { name: /request account deletion/i })).toBeInTheDocument() 463 }) 464 465 await fireEvent.click(screen.getByRole('button', { name: /request account deletion/i })) 466 467 await waitFor(() => { 468 const cancelButtons = screen.getAllByRole('button', { name: /cancel/i }) 469 expect(cancelButtons.length).toBeGreaterThan(0) 470 }) 471 472 const deleteHeading = screen.getByRole('heading', { name: /delete account/i }) 473 const deleteSection = deleteHeading.closest('section') 474 const cancelButton = deleteSection?.querySelector('button.secondary') 475 if (cancelButton) { 476 await fireEvent.click(cancelButton) 477 } 478 479 await waitFor(() => { 480 expect(screen.getByRole('button', { name: /request account deletion/i })).toBeInTheDocument() 481 }) 482 }) 483 484 it('shows error when deletion fails', async () => { 485 window.confirm = vi.fn(() => true) 486 487 mockEndpoint('com.atproto.server.requestAccountDelete', () => 488 jsonResponse({}) 489 ) 490 491 mockEndpoint('com.atproto.server.deleteAccount', () => 492 errorResponse('InvalidToken', 'Invalid confirmation code', 400) 493 ) 494 495 render(Settings) 496 497 await waitFor(() => { 498 expect(screen.getByRole('button', { name: /request account deletion/i })).toBeInTheDocument() 499 }) 500 501 await fireEvent.click(screen.getByRole('button', { name: /request account deletion/i })) 502 503 await waitFor(() => { 504 expect(screen.getByLabelText(/confirmation code/i)).toBeInTheDocument() 505 }) 506 507 await fireEvent.input(screen.getByLabelText(/confirmation code/i), { target: { value: 'WRONG' } }) 508 await fireEvent.input(screen.getByLabelText(/your password/i), { target: { value: 'password' } }) 509 await fireEvent.click(screen.getByRole('button', { name: /permanently delete account/i })) 510 511 await waitFor(() => { 512 expect(screen.getByText(/invalid confirmation code/i)).toBeInTheDocument() 513 }) 514 }) 515 }) 516})