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