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