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});