Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions js/src/app/router/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { RequireAdmin } from "@/app/router/guards/RequireAdmin";
import { RequireAuth } from "@/app/router/guards/RequireAuth";
import AdminPage from "@/features/admin/Admin.page";
import AdminLoginPage from "@/features/admin/AdminLogin.page";
import EmailAdminPage from "@/features/emails/EmailAdminPage";
import AdminEmailComposerPage from "@/features/emails/AdminEmailComposer.page";
import HomePage from "@/features/home/Home.page";
import SamplePage from "@/features/sample/Sample.page";
import SampleAdminPage from "@/features/sample/SampleAdmin.page";
Expand All @@ -20,11 +20,6 @@ export const router = createBrowserRouter([
element: <PublicLayout />,
children: [{ index: true, element: <HomePage /> }],
},
{
element: <EmailAdminPage />,
children: [{ path: "email/admin", element: <EmailAdminPage /> }],
},

// Authenticated: guard -> layout -> page. Admin nests a second guard + layout.
{
element: <RequireAuth />,
Expand All @@ -40,6 +35,7 @@ export const router = createBrowserRouter([
element: <AdminLayout />,
children: [
{ path: "admin", element: <AdminPage /> },
{ path: "admin/emails", element: <AdminEmailComposerPage /> },
{ path: "sample/admin", element: <SampleAdminPage /> },
],
},
Expand Down
105 changes: 105 additions & 0 deletions js/src/features/emails/AdminEmailComposer.page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import AdminEmailComposerPage from "@/features/emails/AdminEmailComposer.page";
import {
renderWithProviders,
screen,
waitFor,
within,
} from "@/lib/test/render";
import { server } from "@/lib/test/server";
import userEvent from "@testing-library/user-event";
import { http, HttpResponse } from "msw";
import { expect, test } from "vitest";

async function selectTemplate(
user: ReturnType<typeof userEvent.setup>,
name: string,
) {
await user.click(screen.getByPlaceholderText("Select email template"));
await user.click(await screen.findByText(name));
await screen.findByText("4 recipients");
}

test("requires a template before opening confirmation", async () => {
const user = userEvent.setup();

renderWithProviders(<AdminEmailComposerPage />);

await user.click(screen.getByRole("button", { name: /review send/i }));

expect(
await screen.findByText("Email template is required"),
).toBeInTheDocument();
});

test("renders a paired template and sends its backend batch payload", async () => {
const user = userEvent.setup();

renderWithProviders(<AdminEmailComposerPage />);

await selectTemplate(user, "Pair");

expect(screen.getAllByText("Matched users")).toHaveLength(2);
expect(screen.getByText("4 recipients")).toBeInTheDocument();
expect(screen.getByLabelText("Plain text body")).toHaveAttribute("readonly");
expect(screen.getAllByText(/Alice and Bob/)).toHaveLength(2);

await user.click(screen.getByRole("button", { name: /review send/i }));

const dialog = await screen.findByRole("dialog", { name: /confirm send/i });

expect(within(dialog).getByText("Matched users")).toBeInTheDocument();
expect(within(dialog).getByText("4")).toBeInTheDocument();

await user.click(screen.getByRole("button", { name: /send email/i }));

expect(await screen.findByText("Email send complete")).toBeInTheDocument();
expect(screen.getByText("Sent 2, failed 0")).toBeInTheDocument();
await waitFor(() => {
expect(screen.queryByText("Confirm send")).not.toBeInTheDocument();
});
});

test("shows failed recipient addresses returned by the backend", async () => {
const user = userEvent.setup();

server.use(
http.post("/api/email/send", () =>
HttpResponse.json({
message: "Sent 1 of 2 emails",
payload: {
failed: 1,
results: [
{
error: null,
recipients: ["alice@example.com", "bob@example.com"],
sent: true,
},
{
error: "Mailbox unavailable",
recipients: ["carol@example.com", "david@example.com"],
sent: false,
},
],
sent: 1,
},
success: true,
}),
),
);

renderWithProviders(<AdminEmailComposerPage />);

await selectTemplate(user, "Pair");
await user.click(screen.getByRole("button", { name: /review send/i }));
await user.click(await screen.findByRole("button", { name: /send email/i }));

expect(
await screen.findByText("Email send completed with failures"),
).toBeInTheDocument();
expect(screen.getByText("Sent 1, failed 1")).toBeInTheDocument();
expect(
screen.getByText(
/carol@example.com, david@example.com: Mailbox unavailable/,
),
).toBeInTheDocument();
});
Loading