Send the invitation as a light letter that names the community, the inviter by email, and who can read the Rule.
This commit is contained in:
1 parent
70ceb73104
commit
727a92a9ac
14 files changed
+534
-26
No files matched your search
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
clipCommunityInviteDescription,
|
||||
communityInviteInitials,
|
||||
communityPossessive,
|
||||
inviteRuleCountsLabel,
|
||||
} from "../../lib/create/ruleInviteCardMeta";
|
||||
|
||||
describe("communityInviteInitials", () => {
|
||||
it("uses the first two words", () => {
|
||||
expect(communityInviteInitials("Juniper Street Food Share")).toBe("JS");
|
||||
});
|
||||
|
||||
it("uses two letters of a single word", () => {
|
||||
expect(communityInviteInitials("Garden")).toBe("GA");
|
||||
});
|
||||
});
|
||||
|
||||
describe("communityPossessive", () => {
|
||||
it("adds a curly ’s when the name does not end in s", () => {
|
||||
expect(communityPossessive("Juniper Street Food Share")).toBe(
|
||||
"Juniper Street Food Share’s",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds only a curly apostrophe when the name ends in s", () => {
|
||||
expect(communityPossessive("Members")).toBe("Members’");
|
||||
});
|
||||
});
|
||||
|
||||
describe("clipCommunityInviteDescription", () => {
|
||||
it("cuts at 140 characters", () => {
|
||||
const long = "a".repeat(160);
|
||||
expect(clipCommunityInviteDescription(long)).toHaveLength(140);
|
||||
});
|
||||
});
|
||||
|
||||
describe("inviteRuleCountsLabel", () => {
|
||||
it("counts values and other cards, not shared terms", () => {
|
||||
expect(
|
||||
inviteRuleCountsLabel({
|
||||
sections: [
|
||||
{
|
||||
categoryName: "Values",
|
||||
entries: [
|
||||
{ title: "Care", body: "We care." },
|
||||
{ title: "Share", body: "We share." },
|
||||
{ title: "Listen", body: "We listen." },
|
||||
],
|
||||
},
|
||||
{
|
||||
categoryName: "Communication",
|
||||
entries: [{ title: "Open channel", body: "We talk in the hall." }],
|
||||
},
|
||||
{
|
||||
categoryName: "Membership",
|
||||
entries: [{ title: "Open admission", body: "Anyone who shares the work." }],
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toBe("3 VALUES AND 2 CARDS");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
RULE_STAKEHOLDER_INVITE_LOGO_PATH,
|
||||
buildRuleStakeholderInviteMail,
|
||||
ruleStakeholderInviteCopyUrl,
|
||||
} from "../../lib/server/ruleStakeholderInviteMail";
|
||||
|
||||
const VERIFY_URL =
|
||||
"https://communityrule.info/api/invites/rule-stakeholder/verify?token=abc";
|
||||
|
||||
describe("ruleStakeholderInviteCopyUrl", () => {
|
||||
it("adds copy=1 to the invitation link", () => {
|
||||
expect(ruleStakeholderInviteCopyUrl(VERIFY_URL)).toBe(
|
||||
`${VERIFY_URL}©=1`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildRuleStakeholderInviteMail", () => {
|
||||
const mail = buildRuleStakeholderInviteMail({
|
||||
to: "rosa@example.org",
|
||||
verifyUrl: VERIFY_URL,
|
||||
origin: "https://communityrule.info",
|
||||
communityName: "Juniper Street Food Share",
|
||||
inviterEmail: "maya@example.org",
|
||||
summary:
|
||||
"A food share that runs every Sunday on Juniper Street. Volunteers collect surplus from two grocers and a farm stand and share it with neighbors.",
|
||||
document: {
|
||||
sections: [
|
||||
{
|
||||
categoryName: "Values",
|
||||
entries: [
|
||||
{ title: "Care", body: "We care." },
|
||||
{ title: "Share", body: "We share." },
|
||||
{ title: "Listen", body: "We listen." },
|
||||
],
|
||||
},
|
||||
{
|
||||
categoryName: "Communication",
|
||||
entries: Array.from({ length: 6 }, (_, i) => ({
|
||||
title: `Card ${i + 1}`,
|
||||
body: "Body",
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
it("uses the launch subject, preview, and privacy line", () => {
|
||||
expect(mail.subject).toBe(
|
||||
"Juniper Street Food Share invited you to read their Rule",
|
||||
);
|
||||
expect(mail.previewText).toBe(
|
||||
"It covers how they talk, who belongs, how they decide and what happens when they disagree.",
|
||||
);
|
||||
expect(mail.text).toContain(
|
||||
"Every Rule starts private. Only the people Juniper Street Food Share invites can read it until the person who holds the pen turns on a link. A Rule is public and featured only when a community asks us in writing.",
|
||||
);
|
||||
expect(mail.html).toContain("Every Rule starts private.");
|
||||
});
|
||||
|
||||
it("names the inviter by email and points replies at that address in copy", () => {
|
||||
expect(mail.text).toContain("maya@example.org sent the invitation.");
|
||||
expect(mail.text).toContain(
|
||||
"how they talk, who belongs, how they decide and what happens when they disagree.",
|
||||
);
|
||||
expect(mail.html).not.toContain("Maya Ortiz");
|
||||
});
|
||||
|
||||
it("includes read and copy links, footer, and the 2x mark", () => {
|
||||
expect(mail.text).toContain(VERIFY_URL);
|
||||
expect(mail.text).toContain(`${VERIFY_URL}©=1`);
|
||||
expect(mail.html).toContain(`href="${VERIFY_URL}"`);
|
||||
expect(mail.html).toContain(`href="${VERIFY_URL}&copy=1"`);
|
||||
expect(mail.html).toContain("Read the Rule");
|
||||
expect(mail.html).toContain("Make a copy");
|
||||
expect(mail.html).toContain("3 VALUES AND 6 CARDS");
|
||||
expect(mail.html).toContain("JS");
|
||||
expect(mail.html).toContain("#e4e4e4");
|
||||
expect(mail.html).toContain("font-size:14px;line-height:20px;color:#464646");
|
||||
expect(mail.html).toContain(
|
||||
`https://communityrule.info${RULE_STAKEHOLDER_INVITE_LOGO_PATH}`,
|
||||
);
|
||||
expect(mail.html).toContain("https://communityrule.info/privacy");
|
||||
expect(mail.html).toContain("https://communityrule.info/about");
|
||||
expect(mail.text).toContain(
|
||||
"Juniper Street Food Share’s Rule on CommunityRule",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -148,9 +148,14 @@ describe("POST /api/rules", () => {
|
||||
expect(res.status).toBe(200);
|
||||
expect(transactionMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendInviteMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendInviteMock.mock.calls[0][0]).toBe("stakeholder@example.com");
|
||||
expect(String(sendInviteMock.mock.calls[0][1])).toContain(
|
||||
"/api/invites/rule-stakeholder/verify?token=",
|
||||
expect(sendInviteMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
to: "stakeholder@example.com",
|
||||
communityName: "Published title",
|
||||
verifyUrl: expect.stringContaining(
|
||||
"/api/invites/rule-stakeholder/verify?token=",
|
||||
),
|
||||
}),
|
||||
);
|
||||
expect(publishedRuleCreateMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -130,7 +130,13 @@ describe("POST /api/rules/[id]/stakeholders", () => {
|
||||
routeCtx,
|
||||
);
|
||||
expect(res.status).toBe(201);
|
||||
expect(createInviteMock).toHaveBeenCalled();
|
||||
expect(createInviteMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
email: "inv@example.com",
|
||||
ruleTitle: "My rule",
|
||||
inviterEmail: owner.email,
|
||||
}),
|
||||
);
|
||||
const body = (await res.json()) as {
|
||||
stakeholder: { email: string; status: string };
|
||||
};
|
||||
@@ -214,9 +220,14 @@ describe("POST /api/rules/[id]/stakeholders/[stakeholderId]/resend", () => {
|
||||
expect(await res.json()).toEqual({ ok: true });
|
||||
expect(updateMock).toHaveBeenCalled();
|
||||
expect(sendInviteEmailMock).toHaveBeenCalledWith(
|
||||
"inv@example.com",
|
||||
expect.stringContaining("/api/invites/rule-stakeholder/verify?token="),
|
||||
"My rule",
|
||||
expect.objectContaining({
|
||||
to: "inv@example.com",
|
||||
communityName: "My rule",
|
||||
inviterEmail: owner.email,
|
||||
verifyUrl: expect.stringContaining(
|
||||
"/api/invites/rule-stakeholder/verify?token=",
|
||||
),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user