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