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