Invite people after review, then finish the Rule, with login before inviting and Invite your members on Profile.

This commit is contained in:
adilallo committed 2026-11-06 08:56:13 -07:00
1 parent 88bfccd1e7
commit 70ceb73104
31 files changed
+1388 -401

No files matched your search

+23 -13
View File
@@ -67,7 +67,7 @@ describe("CompletedScreen", () => {
expect(screen.queryByText("Values")).not.toBeInTheDocument();
});
it("does not pin the title on small screens", () => {
it("keeps the heading in place while the Rule column scrolls", () => {
sessionStorage.setItem(
CREATE_FLOW_LAST_PUBLISHED_KEY,
JSON.stringify(storedRuleFixture),
@@ -78,11 +78,25 @@ describe("CompletedScreen", () => {
});
const column = heading.closest("div[class*='md:justify-center']");
expect(column).toBeTruthy();
expect(column.className).not.toMatch(/\bsticky\b/);
expect(column.className).toMatch(/\bshrink-0\b/);
const ruleColumn = screen
.getByText("Fixture value title")
.closest("div[class*='md:overflow-y-auto']");
expect(ruleColumn?.className).toMatch(/max-md:shrink-0/);
.closest("div[class*='overflow-y-auto']");
expect(ruleColumn?.className).toMatch(/overflow-y-auto/);
});
it("offers Ask an organizer under the finished Rule heading", () => {
sessionStorage.setItem(
CREATE_FLOW_LAST_PUBLISHED_KEY,
JSON.stringify(storedRuleFixture),
);
render(<CompletedScreen />);
expect(
screen.getByText("Has your Rule changed how your community works?"),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Tell an organizer what changed." }),
).toBeInTheDocument();
});
it("renders header and document from sessionStorage", () => {
@@ -106,12 +120,12 @@ describe("CompletedScreen", () => {
render(<CompletedScreen />);
expect(
screen.queryByText(
"This is what folks see when you share your CommunityRule",
"Your Rule is finished",
),
).not.toBeInTheDocument();
expect(
screen.queryByText(
"Your group can use this document as an operating manual.",
"This is what the people you invite will see.",
),
).not.toBeInTheDocument();
});
@@ -123,20 +137,16 @@ describe("CompletedScreen", () => {
});
render(<CompletedScreen />);
expect(
screen.getByText(
"This is what folks see when you share your CommunityRule",
),
screen.getByText("Your Rule is finished"),
).toBeInTheDocument();
expect(
screen.getByText(
"Your group can use this document as an operating manual.",
),
screen.getByText("This is what the people you invite will see."),
).toBeInTheDocument();
const statusRegions = screen.getAllByRole("status");
expect(statusRegions.length).toBeGreaterThanOrEqual(1);
expect(
statusRegions.some((el) =>
el.textContent?.includes("This is what folks see when you share"),
el.textContent?.includes("Your Rule is finished"),
),
).toBe(true);
});
+106 -20
View File
@@ -1,42 +1,128 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { useEffect } from "react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders as render, screen } from "../utils/test-utils";
import "@testing-library/jest-dom/vitest";
import { ConfirmStakeholdersScreen } from "../../app/(app)/create/screens/select/ConfirmStakeholdersScreen";
import { useCreateFlow } from "../../app/(app)/create/context/CreateFlowContext";
import type { CreateFlowState } from "../../app/(app)/create/types";
vi.mock("../../lib/create/api", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../lib/create/api")>();
return {
...actual,
fetchAuthSession: vi.fn(async () => ({
user: { id: "u1", email: "maya@example.org" },
})),
};
});
function SeedCreateFlow({ patch }: { patch: Partial<CreateFlowState> }) {
const { updateState } = useCreateFlow();
useEffect(() => {
updateState(patch);
}, [patch, updateState]);
return null;
}
describe("ConfirmStakeholdersScreen", () => {
it("renders title and description", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders the invite heading and email field", () => {
render(<ConfirmStakeholdersScreen />);
expect(
screen.getByRole("heading", {
name: /Do other stakeholders need to be involved/i,
name: "Invite people to read your Rule",
}),
).toBeInTheDocument();
expect(
screen.getByText("Everyone you add gets a private link to read the Rule."),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Add" }),
).toBeInTheDocument();
expect(
screen.getByText(
/Add their email addresses\. When you publish, we'll send each person a one-time link/i,
/Every Rule starts private\. Only the people you invite can read it/,
),
).toBeInTheDocument();
});
it("renders Add stakeholder control", () => {
it("does not show the old draft toast", () => {
render(<ConfirmStakeholdersScreen />);
expect(
screen.getByRole("button", { name: "Add stakeholder" }),
).toBeInTheDocument();
});
it("shows draft toast and can dismiss it", async () => {
const user = userEvent.setup();
render(<ConfirmStakeholdersScreen />);
expect(
screen.getByText(/Congratulations! You've drafted your CommunityRule!/i),
).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Close" }));
expect(
screen.queryByText(
/Congratulations! You've drafted your CommunityRule!/i,
),
screen.queryByText(/Congratulations! You've drafted your CommunityRule!/i),
).not.toBeInTheDocument();
});
it("adds an email and shows the owner as You", async () => {
const user = userEvent.setup();
render(<ConfirmStakeholdersScreen />);
expect(await screen.findByText("maya@example.org")).toBeInTheDocument();
expect(screen.getByText("You")).toBeInTheDocument();
await user.type(
screen.getByRole("textbox", { name: /Add someone by email/i }),
"rosa@example.org",
);
await user.click(screen.getByRole("button", { name: "Add" }));
expect(screen.getByText("rosa@example.org")).toBeInTheDocument();
expect(
screen.getByText("2 people get a private link to read the Rule."),
).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: /Remove maya@example.org/i }),
).not.toBeInTheDocument();
expect(
screen.getByRole("button", { name: /Remove rosa@example.org/i }),
).toBeInTheDocument();
});
it("keeps a pasted invalid address in the field and reports what was added", async () => {
const user = userEvent.setup();
render(<ConfirmStakeholdersScreen />);
const field = await screen.findByRole("textbox", {
name: /Add someone by email/i,
});
field.focus();
await user.paste("rosa@example.org, jo@example, sam@example.org");
expect(await screen.findByText("rosa@example.org")).toBeInTheDocument();
expect(screen.getByText("sam@example.org")).toBeInTheDocument();
expect(field).toHaveValue("jo@example");
expect(
screen.getByText(
/Added two people from your list\. One is not an email address\./,
),
).toBeInTheDocument();
expect(
screen.getByText("Not an email address. Fix it and select Add, or clear it."),
).toBeInTheDocument();
});
it("shows the membership card from the selected method", async () => {
render(
<>
<SeedCreateFlow
patch={{
selectedMembershipMethodIds: ["open-admission"],
membershipMethodDetailsById: {
"open-admission": {
eligibility: "Anyone who shares the work belongs.",
joiningProcess: "Work two Sunday shares, then ask to join.",
expectations: "",
},
},
}}
/>
<ConfirmStakeholdersScreen />
</>,
);
expect(await screen.findByText("Open admission")).toBeInTheDocument();
expect(screen.getByText("Who belongs, in your Rule’s words")).toBeInTheDocument();
expect(
screen.getByText(/Anyone who shares the work belongs/),
).toBeInTheDocument();
});
});
+38
View File
@@ -212,6 +212,44 @@ describe("LoginForm", () => {
window.localStorage.removeItem("create-flow-anonymous");
});
it("invite variant attaches a create-flow draft", async () => {
const user = userEvent.setup();
window.localStorage.setItem(
"create-flow-anonymous",
JSON.stringify({ title: "Guest draft" }),
);
vi.mocked(requestMagicLink).mockResolvedValue({ ok: true });
renderWithProviders(
<Suspense fallback={null}>
<LoginForm
variant="invite"
magicLinkNextPath="/create/confirm-stakeholders?syncDraft=1"
/>
</Suspense>,
);
expect(
screen.getByRole("heading", {
name: /log in to invite your members/i,
}),
).toBeInTheDocument();
await user.type(
screen.getByRole("textbox", { name: /email address/i }),
"guest@example.com",
);
await user.click(
screen.getByRole("button", { name: /send me a magic link/i }),
);
await waitFor(() => {
expect(requestMagicLink).toHaveBeenCalledWith(
"guest@example.com",
"/create/confirm-stakeholders?syncDraft=1",
expect.objectContaining({ title: "Guest draft" }),
);
});
expect(setTransferPendingFlag).toHaveBeenCalled();
window.localStorage.removeItem("create-flow-anonymous");
});
it("keepRule continue without saving calls onDismiss", async () => {
const user = userEvent.setup();
const onDismiss = vi.fn();