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