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' 13describe('Settings', () => { 14 beforeEach(() => { 15 clearMocks() 16 setupFetchMock() 17 window.confirm = vi.fn(() => true) 18 }) 19 describe('authentication guard', () => { 20 it('redirects to login when not authenticated', async () => { 21 setupUnauthenticatedUser() 22 render(Settings) 23 await waitFor(() => { 24 expect(window.location.hash).toBe('#/login') 25 }) 26 }) 27 }) 28 describe('page structure', () => { 29 beforeEach(() => { 30 setupAuthenticatedUser() 31 }) 32 it('displays all page elements and sections', async () => { 33 render(Settings) 34 await waitFor(() => { 35 expect(screen.getByRole('heading', { name: /account settings/i, level: 1 })).toBeInTheDocument() 36 expect(screen.getByRole('link', { name: /dashboard/i })).toHaveAttribute('href', '#/dashboard') 37 expect(screen.getByRole('heading', { name: /change email/i })).toBeInTheDocument() 38 expect(screen.getByRole('heading', { name: /change handle/i })).toBeInTheDocument() 39 expect(screen.getByRole('heading', { name: /delete account/i })).toBeInTheDocument() 40 }) 41 }) 42 }) 43 describe('email change', () => { 44 beforeEach(() => { 45 setupAuthenticatedUser() 46 }) 47 it('displays current email and input field', async () => { 48 render(Settings) 49 await waitFor(() => { 50 expect(screen.getByText(/current: test@example.com/i)).toBeInTheDocument() 51 expect(screen.getByLabelText(/new email/i)).toBeInTheDocument() 52 }) 53 }) 54 it('calls requestEmailUpdate when submitting', async () => { 55 let requestCalled = false 56 mockEndpoint('com.atproto.server.requestEmailUpdate', () => { 57 requestCalled = true 58 return jsonResponse({ tokenRequired: true }) 59 }) 60 render(Settings) 61 await waitFor(() => { 62 expect(screen.getByLabelText(/new email/i)).toBeInTheDocument() 63 }) 64 await fireEvent.input(screen.getByLabelText(/new email/i), { target: { value: 'newemail@example.com' } }) 65 await fireEvent.click(screen.getByRole('button', { name: /change email/i })) 66 await waitFor(() => { 67 expect(requestCalled).toBe(true) 68 }) 69 }) 70 it('shows verification code input when token is required', async () => { 71 mockEndpoint('com.atproto.server.requestEmailUpdate', () => 72 jsonResponse({ tokenRequired: true }) 73 ) 74 render(Settings) 75 await waitFor(() => { 76 expect(screen.getByLabelText(/new email/i)).toBeInTheDocument() 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 await waitFor(() => { 81 expect(screen.getByLabelText(/verification code/i)).toBeInTheDocument() 82 expect(screen.getByRole('button', { name: /confirm email change/i })).toBeInTheDocument() 83 }) 84 }) 85 it('calls updateEmail with token when confirming', async () => { 86 let updateCalled = false 87 let capturedBody: Record<string, string> | null = null 88 mockEndpoint('com.atproto.server.requestEmailUpdate', () => 89 jsonResponse({ tokenRequired: true }) 90 ) 91 mockEndpoint('com.atproto.server.updateEmail', (_url, options) => { 92 updateCalled = true 93 capturedBody = JSON.parse((options?.body as string) || '{}') 94 return jsonResponse({}) 95 }) 96 render(Settings) 97 await waitFor(() => { 98 expect(screen.getByLabelText(/new email/i)).toBeInTheDocument() 99 }) 100 await fireEvent.input(screen.getByLabelText(/new email/i), { target: { value: 'newemail@example.com' } }) 101 await fireEvent.click(screen.getByRole('button', { name: /change email/i })) 102 await waitFor(() => { 103 expect(screen.getByLabelText(/verification code/i)).toBeInTheDocument() 104 }) 105 await fireEvent.input(screen.getByLabelText(/verification code/i), { target: { value: '123456' } }) 106 await fireEvent.click(screen.getByRole('button', { name: /confirm email change/i })) 107 await waitFor(() => { 108 expect(updateCalled).toBe(true) 109 expect(capturedBody?.email).toBe('newemail@example.com') 110 expect(capturedBody?.token).toBe('123456') 111 }) 112 }) 113 it('shows success message after email update', async () => { 114 mockEndpoint('com.atproto.server.requestEmailUpdate', () => 115 jsonResponse({ tokenRequired: true }) 116 ) 117 mockEndpoint('com.atproto.server.updateEmail', () => 118 jsonResponse({}) 119 ) 120 render(Settings) 121 await waitFor(() => { 122 expect(screen.getByLabelText(/new email/i)).toBeInTheDocument() 123 }) 124 await fireEvent.input(screen.getByLabelText(/new email/i), { target: { value: 'new@test.com' } }) 125 await fireEvent.click(screen.getByRole('button', { name: /change email/i })) 126 await waitFor(() => { 127 expect(screen.getByLabelText(/verification code/i)).toBeInTheDocument() 128 }) 129 await fireEvent.input(screen.getByLabelText(/verification code/i), { target: { value: '123456' } }) 130 await fireEvent.click(screen.getByRole('button', { name: /confirm email change/i })) 131 await waitFor(() => { 132 expect(screen.getByText(/email updated successfully/i)).toBeInTheDocument() 133 }) 134 }) 135 it('shows cancel button to return to email form', async () => { 136 mockEndpoint('com.atproto.server.requestEmailUpdate', () => 137 jsonResponse({ tokenRequired: true }) 138 ) 139 render(Settings) 140 await waitFor(() => { 141 expect(screen.getByLabelText(/new email/i)).toBeInTheDocument() 142 }) 143 await fireEvent.input(screen.getByLabelText(/new email/i), { target: { value: 'new@test.com' } }) 144 await fireEvent.click(screen.getByRole('button', { name: /change email/i })) 145 await waitFor(() => { 146 expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument() 147 }) 148 await fireEvent.click(screen.getByRole('button', { name: /cancel/i })) 149 await waitFor(() => { 150 expect(screen.getByLabelText(/new email/i)).toBeInTheDocument() 151 expect(screen.queryByLabelText(/verification code/i)).not.toBeInTheDocument() 152 }) 153 }) 154 it('shows error when email update fails', async () => { 155 mockEndpoint('com.atproto.server.requestEmailUpdate', () => 156 errorResponse('InvalidEmail', 'Invalid email format', 400) 157 ) 158 render(Settings) 159 await waitFor(() => { 160 expect(screen.getByLabelText(/new email/i)).toBeInTheDocument() 161 }) 162 await fireEvent.input(screen.getByLabelText(/new email/i), { target: { value: 'invalid@test.com' } }) 163 await waitFor(() => { 164 expect(screen.getByRole('button', { name: /change email/i })).not.toBeDisabled() 165 }) 166 await fireEvent.click(screen.getByRole('button', { name: /change email/i })) 167 await waitFor(() => { 168 expect(screen.getByText(/invalid email format/i)).toBeInTheDocument() 169 }) 170 }) 171 }) 172 describe('handle change', () => { 173 beforeEach(() => { 174 setupAuthenticatedUser() 175 }) 176 it('displays current handle', async () => { 177 render(Settings) 178 await waitFor(() => { 179 expect(screen.getByText(/current: @testuser\.test\.bspds\.dev/i)).toBeInTheDocument() 180 }) 181 }) 182 it('calls updateHandle with new handle', async () => { 183 let capturedHandle: string | null = null 184 mockEndpoint('com.atproto.identity.updateHandle', (_url, options) => { 185 const body = JSON.parse((options?.body as string) || '{}') 186 capturedHandle = body.handle 187 return jsonResponse({}) 188 }) 189 render(Settings) 190 await waitFor(() => { 191 expect(screen.getByLabelText(/new handle/i)).toBeInTheDocument() 192 }) 193 await fireEvent.input(screen.getByLabelText(/new handle/i), { target: { value: 'newhandle.bsky.social' } }) 194 await fireEvent.click(screen.getByRole('button', { name: /change handle/i })) 195 await waitFor(() => { 196 expect(capturedHandle).toBe('newhandle.bsky.social') 197 }) 198 }) 199 it('shows success message after handle change', async () => { 200 mockEndpoint('com.atproto.identity.updateHandle', () => 201 jsonResponse({}) 202 ) 203 render(Settings) 204 await waitFor(() => { 205 expect(screen.getByLabelText(/new handle/i)).toBeInTheDocument() 206 }) 207 await fireEvent.input(screen.getByLabelText(/new handle/i), { target: { value: 'newhandle' } }) 208 await fireEvent.click(screen.getByRole('button', { name: /change handle/i })) 209 await waitFor(() => { 210 expect(screen.getByText(/handle updated successfully/i)).toBeInTheDocument() 211 }) 212 }) 213 it('shows error when handle change fails', async () => { 214 mockEndpoint('com.atproto.identity.updateHandle', () => 215 errorResponse('HandleNotAvailable', 'Handle is already taken', 400) 216 ) 217 render(Settings) 218 await waitFor(() => { 219 expect(screen.getByLabelText(/new handle/i)).toBeInTheDocument() 220 }) 221 await fireEvent.input(screen.getByLabelText(/new handle/i), { target: { value: 'taken' } }) 222 await fireEvent.click(screen.getByRole('button', { name: /change handle/i })) 223 await waitFor(() => { 224 expect(screen.getByText(/handle is already taken/i)).toBeInTheDocument() 225 }) 226 }) 227 }) 228 describe('account deletion', () => { 229 beforeEach(() => { 230 setupAuthenticatedUser() 231 mockEndpoint('com.atproto.server.deleteSession', () => 232 jsonResponse({}) 233 ) 234 }) 235 it('displays delete section with warning and request button', async () => { 236 render(Settings) 237 await waitFor(() => { 238 expect(screen.getByText(/this action is irreversible/i)).toBeInTheDocument() 239 expect(screen.getByRole('button', { name: /request account deletion/i })).toBeInTheDocument() 240 }) 241 }) 242 it('calls requestAccountDelete when clicking request', async () => { 243 let requestCalled = false 244 mockEndpoint('com.atproto.server.requestAccountDelete', () => { 245 requestCalled = true 246 return jsonResponse({}) 247 }) 248 render(Settings) 249 await waitFor(() => { 250 expect(screen.getByRole('button', { name: /request account deletion/i })).toBeInTheDocument() 251 }) 252 await fireEvent.click(screen.getByRole('button', { name: /request account deletion/i })) 253 await waitFor(() => { 254 expect(requestCalled).toBe(true) 255 }) 256 }) 257 it('shows confirmation form after requesting deletion', async () => { 258 mockEndpoint('com.atproto.server.requestAccountDelete', () => 259 jsonResponse({}) 260 ) 261 render(Settings) 262 await waitFor(() => { 263 expect(screen.getByRole('button', { name: /request account deletion/i })).toBeInTheDocument() 264 }) 265 await fireEvent.click(screen.getByRole('button', { name: /request account deletion/i })) 266 await waitFor(() => { 267 expect(screen.getByLabelText(/confirmation code/i)).toBeInTheDocument() 268 expect(screen.getByLabelText(/your password/i)).toBeInTheDocument() 269 expect(screen.getByRole('button', { name: /permanently delete account/i })).toBeInTheDocument() 270 }) 271 }) 272 it('shows confirmation dialog before final deletion', async () => { 273 const confirmSpy = vi.fn(() => false) 274 window.confirm = confirmSpy 275 mockEndpoint('com.atproto.server.requestAccountDelete', () => 276 jsonResponse({}) 277 ) 278 render(Settings) 279 await waitFor(() => { 280 expect(screen.getByRole('button', { name: /request account deletion/i })).toBeInTheDocument() 281 }) 282 await fireEvent.click(screen.getByRole('button', { name: /request account deletion/i })) 283 await waitFor(() => { 284 expect(screen.getByLabelText(/confirmation code/i)).toBeInTheDocument() 285 }) 286 await fireEvent.input(screen.getByLabelText(/confirmation code/i), { target: { value: 'ABC123' } }) 287 await fireEvent.input(screen.getByLabelText(/your password/i), { target: { value: 'password' } }) 288 await fireEvent.click(screen.getByRole('button', { name: /permanently delete account/i })) 289 expect(confirmSpy).toHaveBeenCalledWith( 290 expect.stringContaining('absolutely sure') 291 ) 292 }) 293 it('calls deleteAccount with correct parameters', async () => { 294 window.confirm = vi.fn(() => true) 295 let capturedBody: Record<string, string> | null = null 296 mockEndpoint('com.atproto.server.requestAccountDelete', () => 297 jsonResponse({}) 298 ) 299 mockEndpoint('com.atproto.server.deleteAccount', (_url, options) => { 300 capturedBody = JSON.parse((options?.body as string) || '{}') 301 return jsonResponse({}) 302 }) 303 render(Settings) 304 await waitFor(() => { 305 expect(screen.getByRole('button', { name: /request account deletion/i })).toBeInTheDocument() 306 }) 307 await fireEvent.click(screen.getByRole('button', { name: /request account deletion/i })) 308 await waitFor(() => { 309 expect(screen.getByLabelText(/confirmation code/i)).toBeInTheDocument() 310 }) 311 await fireEvent.input(screen.getByLabelText(/confirmation code/i), { target: { value: 'DEL123' } }) 312 await fireEvent.input(screen.getByLabelText(/your password/i), { target: { value: 'mypassword' } }) 313 await fireEvent.click(screen.getByRole('button', { name: /permanently delete account/i })) 314 await waitFor(() => { 315 expect(capturedBody?.token).toBe('DEL123') 316 expect(capturedBody?.password).toBe('mypassword') 317 expect(capturedBody?.did).toBe('did:web:test.bspds.dev:u:testuser') 318 }) 319 }) 320 it('navigates to login after successful deletion', async () => { 321 window.confirm = vi.fn(() => true) 322 mockEndpoint('com.atproto.server.requestAccountDelete', () => 323 jsonResponse({}) 324 ) 325 mockEndpoint('com.atproto.server.deleteAccount', () => 326 jsonResponse({}) 327 ) 328 render(Settings) 329 await waitFor(() => { 330 expect(screen.getByRole('button', { name: /request account deletion/i })).toBeInTheDocument() 331 }) 332 await fireEvent.click(screen.getByRole('button', { name: /request account deletion/i })) 333 await waitFor(() => { 334 expect(screen.getByLabelText(/confirmation code/i)).toBeInTheDocument() 335 }) 336 await fireEvent.input(screen.getByLabelText(/confirmation code/i), { target: { value: 'DEL123' } }) 337 await fireEvent.input(screen.getByLabelText(/your password/i), { target: { value: 'password' } }) 338 await fireEvent.click(screen.getByRole('button', { name: /permanently delete account/i })) 339 await waitFor(() => { 340 expect(window.location.hash).toBe('#/login') 341 }) 342 }) 343 it('shows cancel button to return to request state', async () => { 344 mockEndpoint('com.atproto.server.requestAccountDelete', () => 345 jsonResponse({}) 346 ) 347 render(Settings) 348 await waitFor(() => { 349 expect(screen.getByRole('button', { name: /request account deletion/i })).toBeInTheDocument() 350 }) 351 await fireEvent.click(screen.getByRole('button', { name: /request account deletion/i })) 352 await waitFor(() => { 353 const cancelButtons = screen.getAllByRole('button', { name: /cancel/i }) 354 expect(cancelButtons.length).toBeGreaterThan(0) 355 }) 356 const deleteHeading = screen.getByRole('heading', { name: /delete account/i }) 357 const deleteSection = deleteHeading.closest('section') 358 const cancelButton = deleteSection?.querySelector('button.secondary') 359 if (cancelButton) { 360 await fireEvent.click(cancelButton) 361 } 362 await waitFor(() => { 363 expect(screen.getByRole('button', { name: /request account deletion/i })).toBeInTheDocument() 364 }) 365 }) 366 it('shows error when deletion fails', async () => { 367 window.confirm = vi.fn(() => true) 368 mockEndpoint('com.atproto.server.requestAccountDelete', () => 369 jsonResponse({}) 370 ) 371 mockEndpoint('com.atproto.server.deleteAccount', () => 372 errorResponse('InvalidToken', 'Invalid confirmation code', 400) 373 ) 374 render(Settings) 375 await waitFor(() => { 376 expect(screen.getByRole('button', { name: /request account deletion/i })).toBeInTheDocument() 377 }) 378 await fireEvent.click(screen.getByRole('button', { name: /request account deletion/i })) 379 await waitFor(() => { 380 expect(screen.getByLabelText(/confirmation code/i)).toBeInTheDocument() 381 }) 382 await fireEvent.input(screen.getByLabelText(/confirmation code/i), { target: { value: 'WRONG' } }) 383 await fireEvent.input(screen.getByLabelText(/your password/i), { target: { value: 'password' } }) 384 await fireEvent.click(screen.getByRole('button', { name: /permanently delete account/i })) 385 await waitFor(() => { 386 expect(screen.getByText(/invalid confirmation code/i)).toBeInTheDocument() 387 }) 388 }) 389 }) 390})