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

+6 -1
View File
@@ -183,7 +183,7 @@ describe("flowSteps", () => {
).toBeNull();
});
it("parseReviewReturnSearchParam accepts final-review, edit-rule, and review", () => {
it("parseReviewReturnSearchParam accepts final-review, edit-rule, review, and completed", () => {
expect(
parseReviewReturnSearchParam(
new URLSearchParams("reviewReturn=final-review"),
@@ -197,6 +197,11 @@ describe("flowSteps", () => {
expect(
parseReviewReturnSearchParam(new URLSearchParams("reviewReturn=review")),
).toBe("review");
expect(
parseReviewReturnSearchParam(
new URLSearchParams("reviewReturn=completed"),
),
).toBe("completed");
expect(
parseReviewReturnSearchParam(new URLSearchParams("reviewReturn=nope")),
).toBeNull();
@@ -306,6 +306,71 @@ describe("useCreateFlowFinalize", () => {
);
});
it("omits stakeholder emails when skipInvites is set", async () => {
vi.mocked(publishRule).mockResolvedValue({
ok: true,
id: "new-rule-id",
title: "Published title",
});
const { result } = renderHook(() =>
useCreateFlowFinalize({
state: {
...emptyState,
stakeholderEmails: ["invitee@example.com"],
},
router,
openLogin,
updateState,
loginReturnPath: "/create/final-review",
sessionUser: signedIn,
}),
);
await act(async () => {
await result.current.finalize({ skipInvites: true });
});
expect(publishRule).toHaveBeenCalledWith({
title: "Published title",
summary: "Published summary",
document: {},
});
});
it("uses the send-failed banner when invitations do not send", async () => {
vi.mocked(publishRule).mockResolvedValue({
ok: false,
status: 500,
error: "mail failed",
});
const { result } = renderHook(() =>
useCreateFlowFinalize({
state: {
...emptyState,
stakeholderEmails: ["invitee@example.com"],
},
router,
openLogin,
updateState,
loginReturnPath: "/create/final-review",
sessionUser: signedIn,
}),
);
await act(async () => {
await result.current.finalize({ skipInvites: false });
});
expect(result.current.publishBannerTitle).toBe(
messages.create.reviewAndComplete.confirmStakeholders.sendFailedTitle,
);
expect(result.current.publishBannerMessage).toBe(
messages.create.reviewAndComplete.confirmStakeholders.sendFailedDescription,
);
});
it("routes to /create/completed without celebrate after PATCH update", async () => {
vi.mocked(updatePublishedRule).mockResolvedValue({ ok: true });
@@ -0,0 +1,28 @@
import { describe, expect, it } from "vitest";
import { membershipInvitePreview } from "../../lib/create/membershipInvitePreview";
import type { CreateFlowState } from "../../app/(app)/create/types";
describe("membershipInvitePreview", () => {
it("returns null when no membership method is selected", () => {
expect(membershipInvitePreview({} as CreateFlowState)).toBeNull();
});
it("uses the first selected membership method and saved words", () => {
const preview = membershipInvitePreview({
selectedMembershipMethodIds: ["open-admission"],
membershipMethodDetailsById: {
"open-admission": {
eligibility: "Anyone who shares the work belongs.",
joiningProcess: "Work two Sunday shares, then ask to join.",
expectations: "",
},
},
} as CreateFlowState);
expect(preview?.title).toBe("Open admission");
expect(preview?.categoryName).toBe("Membership");
expect(preview?.eligibility).toBe("Anyone who shares the work belongs.");
expect(preview?.joiningProcess).toBe(
"Work two Sunday shares, then ask to join.",
);
});
});
+54
View File
@@ -0,0 +1,54 @@
import { describe, expect, it } from "vitest";
import {
formatInviteCountWord,
parseInvitePaste,
splitInvitePasteTokens,
} from "../../lib/create/parseInviteEmails";
describe("parseInviteEmails", () => {
it("splits comma, semicolon, and whitespace lists", () => {
expect(
splitInvitePasteTokens("rosa@example.org, dev@example.org; sam@example.org\nlee@example.org"),
).toEqual([
"rosa@example.org",
"dev@example.org",
"sam@example.org",
"lee@example.org",
]);
});
it("adds valid addresses and leaves invalid tokens in the remainder", () => {
const result = parseInvitePaste(
"rosa@example.org, jo@example, sam@example.org",
["maya@example.org"],
30,
);
expect(result.added).toEqual(["rosa@example.org", "sam@example.org"]);
expect(result.invalidRemainder).toBe("jo@example");
});
it("skips duplicates already on the list", () => {
const result = parseInvitePaste(
"maya@example.org, rosa@example.org",
["maya@example.org"],
30,
);
expect(result.added).toEqual(["rosa@example.org"]);
expect(result.skippedDuplicate).toBe(1);
});
it("keeps overflow addresses in the remainder once the max is hit", () => {
const result = parseInvitePaste(
"a@example.org, b@example.org, c@example.org",
[],
2,
);
expect(result.added).toEqual(["a@example.org", "b@example.org"]);
expect(result.invalidRemainder).toBe("c@example.org");
});
it("uses words for counts through ten", () => {
expect(formatInviteCountWord(5)).toBe("five");
expect(formatInviteCountWord(11)).toBe("11");
});
});