Files
tranquil-pds/frontend/src/tests/Comms.test.ts
T
2025-12-25 19:59:32 +02:00

437 lines
14 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/svelte";
import Comms from "../routes/Comms.svelte";
import {
clearMocks,
errorResponse,
jsonResponse,
mockData,
mockEndpoint,
setupAuthenticatedUser,
setupFetchMock,
setupUnauthenticatedUser,
} from "./mocks";
describe("Comms", () => {
beforeEach(() => {
clearMocks();
setupFetchMock();
});
describe("authentication guard", () => {
it("redirects to login when not authenticated", async () => {
setupUnauthenticatedUser();
render(Comms);
await waitFor(() => {
expect(window.location.hash).toBe("#/login");
});
});
});
describe("page structure", () => {
beforeEach(() => {
setupAuthenticatedUser();
mockEndpoint(
"com.tranquil.account.getNotificationPrefs",
() => jsonResponse(mockData.notificationPrefs()),
);
});
it("displays all page elements and sections", async () => {
render(Comms);
await waitFor(() => {
expect(
screen.getByRole("heading", {
name: /notification preferences/i,
level: 1,
}),
).toBeInTheDocument();
expect(screen.getByRole("link", { name: /dashboard/i }))
.toHaveAttribute("href", "#/dashboard");
expect(screen.getByText(/password resets/i)).toBeInTheDocument();
expect(screen.getByRole("heading", { name: /preferred channel/i }))
.toBeInTheDocument();
expect(screen.getByRole("heading", { name: /channel configuration/i }))
.toBeInTheDocument();
});
});
});
describe("loading state", () => {
beforeEach(() => {
setupAuthenticatedUser();
});
it("shows loading text while fetching preferences", async () => {
mockEndpoint("com.tranquil.account.getNotificationPrefs", async () => {
await new Promise((resolve) => setTimeout(resolve, 100));
return jsonResponse(mockData.notificationPrefs());
});
render(Comms);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
});
});
describe("channel options", () => {
beforeEach(() => {
setupAuthenticatedUser();
});
it("displays all four channel options", async () => {
mockEndpoint(
"com.tranquil.account.getNotificationPrefs",
() => jsonResponse(mockData.notificationPrefs()),
);
render(Comms);
await waitFor(() => {
expect(screen.getByRole("radio", { name: /email/i }))
.toBeInTheDocument();
expect(screen.getByRole("radio", { name: /discord/i }))
.toBeInTheDocument();
expect(screen.getByRole("radio", { name: /telegram/i }))
.toBeInTheDocument();
expect(screen.getByRole("radio", { name: /signal/i }))
.toBeInTheDocument();
});
});
it("email channel is always selectable", async () => {
mockEndpoint(
"com.tranquil.account.getNotificationPrefs",
() => jsonResponse(mockData.notificationPrefs()),
);
render(Comms);
await waitFor(() => {
const emailRadio = screen.getByRole("radio", { name: /email/i });
expect(emailRadio).not.toBeDisabled();
});
});
it("discord channel is disabled when not configured", async () => {
mockEndpoint(
"com.tranquil.account.getNotificationPrefs",
() => jsonResponse(mockData.notificationPrefs({ discordId: null })),
);
render(Comms);
await waitFor(() => {
const discordRadio = screen.getByRole("radio", { name: /discord/i });
expect(discordRadio).toBeDisabled();
});
});
it("discord channel is enabled when configured", async () => {
mockEndpoint(
"com.tranquil.account.getNotificationPrefs",
() =>
jsonResponse(mockData.notificationPrefs({ discordId: "123456789" })),
);
render(Comms);
await waitFor(() => {
const discordRadio = screen.getByRole("radio", { name: /discord/i });
expect(discordRadio).not.toBeDisabled();
});
});
it("shows hint for disabled channels", async () => {
mockEndpoint(
"com.tranquil.account.getNotificationPrefs",
() => jsonResponse(mockData.notificationPrefs()),
);
render(Comms);
await waitFor(() => {
expect(screen.getAllByText(/configure below to enable/i).length)
.toBeGreaterThan(0);
});
});
it("selects current preferred channel", async () => {
mockEndpoint(
"com.tranquil.account.getNotificationPrefs",
() =>
jsonResponse(
mockData.notificationPrefs({ preferredChannel: "email" }),
),
);
render(Comms);
await waitFor(() => {
const emailRadio = screen.getByRole("radio", {
name: /email/i,
}) as HTMLInputElement;
expect(emailRadio.checked).toBe(true);
});
});
});
describe("channel configuration", () => {
beforeEach(() => {
setupAuthenticatedUser();
});
it("displays email as readonly with current value", async () => {
mockEndpoint(
"com.tranquil.account.getNotificationPrefs",
() => jsonResponse(mockData.notificationPrefs()),
);
render(Comms);
await waitFor(() => {
const emailInput = screen.getByLabelText(
/^email$/i,
) as HTMLInputElement;
expect(emailInput).toBeDisabled();
expect(emailInput.value).toBe("test@example.com");
});
});
it("displays all channel inputs with current values", async () => {
mockEndpoint(
"com.tranquil.account.getNotificationPrefs",
() =>
jsonResponse(mockData.notificationPrefs({
discordId: "123456789",
telegramUsername: "testuser",
signalNumber: "+1234567890",
})),
);
render(Comms);
await waitFor(() => {
expect(
(screen.getByLabelText(/discord user id/i) as HTMLInputElement).value,
).toBe("123456789");
expect(
(screen.getByLabelText(/telegram username/i) as HTMLInputElement)
.value,
).toBe("testuser");
expect(
(screen.getByLabelText(/signal phone number/i) as HTMLInputElement)
.value,
).toBe("+1234567890");
});
});
});
describe("verification status badges", () => {
beforeEach(() => {
setupAuthenticatedUser();
});
it("shows Primary badge for email", async () => {
mockEndpoint(
"com.tranquil.account.getNotificationPrefs",
() => jsonResponse(mockData.notificationPrefs()),
);
render(Comms);
await waitFor(() => {
expect(screen.getByText("Primary")).toBeInTheDocument();
});
});
it("shows Verified badge for verified discord", async () => {
mockEndpoint(
"com.tranquil.account.getNotificationPrefs",
() =>
jsonResponse(mockData.notificationPrefs({
discordId: "123456789",
discordVerified: true,
})),
);
render(Comms);
await waitFor(() => {
const verifiedBadges = screen.getAllByText("Verified");
expect(verifiedBadges.length).toBeGreaterThan(0);
});
});
it("shows Not verified badge for unverified discord", async () => {
mockEndpoint(
"com.tranquil.account.getNotificationPrefs",
() =>
jsonResponse(mockData.notificationPrefs({
discordId: "123456789",
discordVerified: false,
})),
);
render(Comms);
await waitFor(() => {
expect(screen.getByText("Not verified")).toBeInTheDocument();
});
});
it("does not show badge when channel not configured", async () => {
mockEndpoint(
"com.tranquil.account.getNotificationPrefs",
() => jsonResponse(mockData.notificationPrefs()),
);
render(Comms);
await waitFor(() => {
expect(screen.getByText("Primary")).toBeInTheDocument();
expect(screen.queryByText("Not verified")).not.toBeInTheDocument();
});
});
});
describe("save preferences", () => {
beforeEach(() => {
setupAuthenticatedUser();
});
it("calls updateNotificationPrefs with correct data", async () => {
let capturedBody: Record<string, unknown> | null = null;
mockEndpoint(
"com.tranquil.account.getNotificationPrefs",
() => jsonResponse(mockData.notificationPrefs()),
);
mockEndpoint(
"com.tranquil.account.updateNotificationPrefs",
(_url, options) => {
capturedBody = JSON.parse((options?.body as string) || "{}");
return jsonResponse({ success: true });
},
);
render(Comms);
await waitFor(() => {
expect(screen.getByLabelText(/discord user id/i)).toBeInTheDocument();
});
await fireEvent.input(screen.getByLabelText(/discord user id/i), {
target: { value: "999888777" },
});
await fireEvent.click(
screen.getByRole("button", { name: /save preferences/i }),
);
await waitFor(() => {
expect(capturedBody).not.toBeNull();
expect(capturedBody?.discordId).toBe("999888777");
expect(capturedBody?.preferredChannel).toBe("email");
});
});
it("shows loading state while saving", async () => {
mockEndpoint(
"com.tranquil.account.getNotificationPrefs",
() => jsonResponse(mockData.notificationPrefs()),
);
mockEndpoint("com.tranquil.account.updateNotificationPrefs", async () => {
await new Promise((resolve) => setTimeout(resolve, 100));
return jsonResponse({ success: true });
});
render(Comms);
await waitFor(() => {
expect(screen.getByRole("button", { name: /save preferences/i }))
.toBeInTheDocument();
});
await fireEvent.click(
screen.getByRole("button", { name: /save preferences/i }),
);
expect(screen.getByRole("button", { name: /saving/i }))
.toBeInTheDocument();
expect(screen.getByRole("button", { name: /saving/i })).toBeDisabled();
});
it("shows success message after saving", async () => {
mockEndpoint(
"com.tranquil.account.getNotificationPrefs",
() => jsonResponse(mockData.notificationPrefs()),
);
mockEndpoint(
"com.tranquil.account.updateNotificationPrefs",
() => jsonResponse({ success: true }),
);
render(Comms);
await waitFor(() => {
expect(screen.getByRole("button", { name: /save preferences/i }))
.toBeInTheDocument();
});
await fireEvent.click(
screen.getByRole("button", { name: /save preferences/i }),
);
await waitFor(() => {
expect(screen.getByText(/notification preferences saved/i))
.toBeInTheDocument();
});
});
it("shows error when save fails", async () => {
mockEndpoint(
"com.tranquil.account.getNotificationPrefs",
() => jsonResponse(mockData.notificationPrefs()),
);
mockEndpoint(
"com.tranquil.account.updateNotificationPrefs",
() =>
errorResponse("InvalidRequest", "Invalid channel configuration", 400),
);
render(Comms);
await waitFor(() => {
expect(screen.getByRole("button", { name: /save preferences/i }))
.toBeInTheDocument();
});
await fireEvent.click(
screen.getByRole("button", { name: /save preferences/i }),
);
await waitFor(() => {
expect(screen.getByText(/invalid channel configuration/i))
.toBeInTheDocument();
expect(
screen.getByText(/invalid channel configuration/i).closest(
".message",
),
).toHaveClass("error");
});
});
it("reloads preferences after successful save", async () => {
let loadCount = 0;
mockEndpoint("com.tranquil.account.getNotificationPrefs", () => {
loadCount++;
return jsonResponse(mockData.notificationPrefs());
});
mockEndpoint(
"com.tranquil.account.updateNotificationPrefs",
() => jsonResponse({ success: true }),
);
render(Comms);
await waitFor(() => {
expect(screen.getByRole("button", { name: /save preferences/i }))
.toBeInTheDocument();
});
const initialLoadCount = loadCount;
await fireEvent.click(
screen.getByRole("button", { name: /save preferences/i }),
);
await waitFor(() => {
expect(loadCount).toBeGreaterThan(initialLoadCount);
});
});
});
describe("channel selection interaction", () => {
beforeEach(() => {
setupAuthenticatedUser();
});
it("enables discord channel after entering discord ID", async () => {
mockEndpoint(
"com.tranquil.account.getNotificationPrefs",
() => jsonResponse(mockData.notificationPrefs()),
);
render(Comms);
await waitFor(() => {
expect(screen.getByRole("radio", { name: /discord/i })).toBeDisabled();
});
await fireEvent.input(screen.getByLabelText(/discord user id/i), {
target: { value: "123456789" },
});
await waitFor(() => {
expect(screen.getByRole("radio", { name: /discord/i })).not
.toBeDisabled();
});
});
it("allows selecting a configured channel", async () => {
mockEndpoint(
"com.tranquil.account.getNotificationPrefs",
() =>
jsonResponse(mockData.notificationPrefs({
discordId: "123456789",
discordVerified: true,
})),
);
render(Comms);
await waitFor(() => {
expect(screen.getByRole("radio", { name: /discord/i })).not
.toBeDisabled();
});
await fireEvent.click(screen.getByRole("radio", { name: /discord/i }));
const discordRadio = screen.getByRole("radio", {
name: /discord/i,
}) as HTMLInputElement;
expect(discordRadio.checked).toBe(true);
});
});
describe("error handling", () => {
beforeEach(() => {
setupAuthenticatedUser();
});
it("shows error when loading preferences fails", async () => {
mockEndpoint(
"com.tranquil.account.getNotificationPrefs",
() => errorResponse("InternalError", "Database connection failed", 500),
);
render(Comms);
await waitFor(() => {
expect(screen.getByText(/database connection failed/i))
.toBeInTheDocument();
});
});
});
});