Ask organizer modal implemented
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders as render, screen } from "../utils/test-utils";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import AskOrganizer from "../../app/components/sections/AskOrganizer";
|
||||
@@ -52,15 +53,24 @@ describe("AskOrganizer (behavioral tests)", () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders button with default text", () => {
|
||||
it("renders CTA button with default label", () => {
|
||||
render(<AskOrganizer title="Test" />);
|
||||
expect(
|
||||
screen.getByRole("link", {
|
||||
screen.getByRole("button", {
|
||||
name: /ask an organizer/i,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens inquiry modal when CTA is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<AskOrganizer title="Test" />);
|
||||
await user.click(screen.getByTestId("ask-organizer-cta"));
|
||||
expect(
|
||||
await screen.findByRole("dialog", { name: /ask an organizer/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders button with custom text", () => {
|
||||
render(
|
||||
<AskOrganizer title="Test" buttonText="Contact" buttonHref="/contact" />,
|
||||
|
||||
@@ -69,15 +69,13 @@ test.describe("Critical User Journeys", () => {
|
||||
// 8. User reads testimonial
|
||||
await expect(page.locator("text=Jo Freeman")).toBeVisible();
|
||||
|
||||
// 9. User decides to contact organizer
|
||||
const askButton = page.locator(
|
||||
'a:has-text("Ask an organizer"), button:has-text("Ask an organizer")',
|
||||
);
|
||||
if (
|
||||
(await askButton.count()) > 0 &&
|
||||
(await askButton.first().isVisible())
|
||||
) {
|
||||
await askButton.first().click();
|
||||
// 9. User decides to contact organizer (opens modal)
|
||||
const askButton = page.getByTestId("ask-organizer-cta").first();
|
||||
if ((await askButton.count()) > 0 && (await askButton.isVisible())) {
|
||||
await askButton.click();
|
||||
await expect(
|
||||
page.getByRole("dialog", { name: /ask an organizer/i }),
|
||||
).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ describe("Page Flow Integration", () => {
|
||||
screen.getByText("Get answers from an experienced organizer"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("link", { name: /Ask an organizer/i }),
|
||||
screen.getByRole("button", { name: /ask an organizer/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -198,9 +198,9 @@ describe("Page Flow Integration", () => {
|
||||
test("ask organizer section has proper call-to-action", () => {
|
||||
render(<Page />);
|
||||
|
||||
const askLink = screen.getByRole("link", { name: /Ask an organizer/i });
|
||||
expect(askLink).toBeInTheDocument();
|
||||
expect(askLink).toHaveAttribute("href", "#contact");
|
||||
const askCta = screen.getByRole("button", { name: /ask an organizer/i });
|
||||
expect(askCta).toBeInTheDocument();
|
||||
expect(askCta).not.toHaveAttribute("href");
|
||||
});
|
||||
|
||||
test("page maintains proper semantic structure", async () => {
|
||||
@@ -223,16 +223,22 @@ describe("Page Flow Integration", () => {
|
||||
expect(mainContent).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("all interactive elements are accessible", () => {
|
||||
test("all interactive elements are accessible", async () => {
|
||||
render(<Page />);
|
||||
|
||||
// Check all buttons have proper roles
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByRole("button").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
const buttons = screen.getAllByRole("button");
|
||||
buttons.forEach((button) => {
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Check all links have proper roles
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByRole("link").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
const links = screen.getAllByRole("link");
|
||||
links.forEach((link) => {
|
||||
expect(link).toBeInTheDocument();
|
||||
|
||||
@@ -121,10 +121,11 @@ describe("User Journey Integration", () => {
|
||||
screen.getByText("Get answers from an experienced organizer"),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// User clicks the ask organizer button (it's actually a link, not a button)
|
||||
const askLink = screen.getByRole("link", { name: /Ask an organizer/i });
|
||||
await user.click(askLink);
|
||||
expect(askLink).toHaveAttribute("href", "#contact");
|
||||
const askCta = screen.getByTestId("ask-organizer-cta");
|
||||
await user.click(askCta);
|
||||
expect(
|
||||
await screen.findByRole("dialog", { name: /ask an organizer/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("user explores the process through CardSteps", async () => {
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const sendOrganizerInquiryNotificationMock = vi.fn();
|
||||
|
||||
vi.mock("../../lib/server/mail", () => ({
|
||||
sendOrganizerInquiryNotification: (...args: unknown[]) =>
|
||||
sendOrganizerInquiryNotificationMock(...args),
|
||||
}));
|
||||
|
||||
const rateLimitKeyMock = vi.hoisted(() =>
|
||||
vi.fn(() => ({ ok: true as const })),
|
||||
);
|
||||
|
||||
vi.mock("../../lib/server/rateLimit", () => ({
|
||||
rateLimitKey: (...args: unknown[]) => rateLimitKeyMock(...args),
|
||||
}));
|
||||
|
||||
import { POST } from "../../app/api/organizer-inquiry/route";
|
||||
|
||||
describe("POST /api/organizer-inquiry", () => {
|
||||
beforeEach(() => {
|
||||
sendOrganizerInquiryNotificationMock.mockReset();
|
||||
sendOrganizerInquiryNotificationMock.mockResolvedValue(undefined);
|
||||
rateLimitKeyMock.mockReset();
|
||||
rateLimitKeyMock.mockImplementation(() => ({ ok: true as const }));
|
||||
process.env.ORGANIZER_INQUIRY_TO = "organizers@example.com";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.ORGANIZER_INQUIRY_TO;
|
||||
});
|
||||
|
||||
it("returns 200 and sends mail for a valid payload", async () => {
|
||||
const res = await POST(
|
||||
new NextRequest("https://x.test/api/organizer-inquiry", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
email: "Visitor@Example.com",
|
||||
message: "How do we run consensus meetings?",
|
||||
company: "",
|
||||
}),
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body).toEqual({ ok: true });
|
||||
expect(sendOrganizerInquiryNotificationMock).toHaveBeenCalledTimes(1);
|
||||
const arg = sendOrganizerInquiryNotificationMock.mock.calls[0][0];
|
||||
expect(arg.visitorEmail).toBe("visitor@example.com");
|
||||
expect(arg.message).toBe("How do we run consensus meetings?");
|
||||
});
|
||||
|
||||
it("returns 200 without sending mail when honeypot is filled", async () => {
|
||||
const res = await POST(
|
||||
new NextRequest("https://x.test/api/organizer-inquiry", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
email: "spam@example.com",
|
||||
message: "How do we run consensus meetings?",
|
||||
company: "Evil Corp",
|
||||
}),
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(sendOrganizerInquiryNotificationMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns 400 for invalid email", async () => {
|
||||
const res = await POST(
|
||||
new NextRequest("https://x.test/api/organizer-inquiry", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
email: "not-an-email",
|
||||
message: "How do we run consensus meetings?",
|
||||
company: "",
|
||||
}),
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(sendOrganizerInquiryNotificationMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns 429 when rate limited", async () => {
|
||||
rateLimitKeyMock.mockReturnValue({
|
||||
ok: false as const,
|
||||
retryAfterMs: 1000,
|
||||
});
|
||||
|
||||
const res = await POST(
|
||||
new NextRequest("https://x.test/api/organizer-inquiry", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
email: "a@b.co",
|
||||
message: "How do we run consensus meetings?",
|
||||
company: "",
|
||||
}),
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(res.status).toBe(429);
|
||||
expect(sendOrganizerInquiryNotificationMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns 500 when ORGANIZER_INQUIRY_TO is unset", async () => {
|
||||
delete process.env.ORGANIZER_INQUIRY_TO;
|
||||
|
||||
const res = await POST(
|
||||
new NextRequest("https://x.test/api/organizer-inquiry", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
email: "a@b.co",
|
||||
message: "How do we run consensus meetings?",
|
||||
company: "",
|
||||
}),
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(res.status).toBe(500);
|
||||
expect(sendOrganizerInquiryNotificationMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user