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