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