Walk Create Community through the Figma screens and restore a chosen logo after reload, so a guest can leave without saving until they reach the email step.

This commit is contained in:
adilallo committed 2026-11-05 16:14:04 -07:00
1 parent ca1f58a50c
commit 2348a9e94f
61 files changed
+1110 -421

No files matched your search

+14
View File
@@ -57,4 +57,18 @@ describe("Alert dismiss control", () => {
screen.queryByRole("button", { name: "Close" }),
).not.toBeInTheDocument();
});
it("renders an action control when actionLabel and onAction are set", async () => {
const user = userEvent.setup();
const onAction = vi.fn();
render(
<Alert
title="Check your email"
actionLabel="Send it again"
onAction={onAction}
/>,
);
await user.click(screen.getByRole("button", { name: "Send it again" }));
expect(onAction).toHaveBeenCalledTimes(1);
});
});
@@ -8,21 +8,25 @@ describe("CommunityStructureSelectScreen", () => {
it("does not render dummy help icons", () => {
renderWithProviders(<CommunityStructureSelectScreen />);
expect(screen.getByText("Organization Type")).toBeInTheDocument();
expect(
screen.getByText("What kind of community are you, mainly?"),
).toBeInTheDocument();
expect(screen.getByText("Anything else that fits?")).toBeInTheDocument();
expect(screen.getByText("Scale")).toBeInTheDocument();
expect(screen.getByText("Maturity")).toBeInTheDocument();
expect(
screen.getByText(/Choose tags that describe your community/i),
screen.getByText(/Your answers shape which cards we suggest/i),
).toBeInTheDocument();
expect(screen.queryByAltText("Help")).not.toBeInTheDocument();
expect(screen.getByText("Pick one")).toBeInTheDocument();
});
it("gives structure chips Select/Deselect accessible names", () => {
renderWithProviders(<CommunityStructureSelectScreen />);
expect(
screen.getByRole("button", { name: "Select Worker co-op" }),
).toBeInTheDocument();
screen.getAllByRole("button", { name: "Select Worker co-op" }).length,
).toBeGreaterThanOrEqual(2);
expect(
screen.getByRole("button", { name: "Select Local" }),
).toBeInTheDocument();
+2 -2
View File
@@ -50,7 +50,7 @@ describe("CreateFlowFooter (behavioral tests)", () => {
});
expect(footer).toBeInTheDocument();
const bar = screen.getByRole("progressbar");
expect(bar).toHaveAttribute("aria-valuenow", String(1 / 6));
expect(bar).toHaveAttribute("aria-valuenow", String(1 / 9));
});
it("passes proportionBarProgress to the progress bar", () => {
@@ -63,7 +63,7 @@ describe("CreateFlowFooter (behavioral tests)", () => {
);
expect(screen.getByRole("progressbar")).toHaveAttribute(
"aria-valuenow",
String(2 / 6),
String(2 / 9),
);
});
+5 -5
View File
@@ -66,9 +66,9 @@ describe("CreateFlowTopNav (behavioral tests)", () => {
expect(exitButton).toBeInTheDocument();
});
it("shows Save & Exit when saveDraftOnExit is true", () => {
it("shows Save and exit when saveDraftOnExit is true", () => {
render(<CreateFlowTopNav saveDraftOnExit={true} />);
const exitButton = screen.getByRole("button", { name: "Save & Exit" });
const exitButton = screen.getByRole("button", { name: "Save and exit" });
expect(exitButton).toBeInTheDocument();
});
@@ -232,7 +232,7 @@ describe("CreateFlowTopNav (viewport < sm2 / 440px)", () => {
).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Edit" })).not.toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Save & Exit" }),
screen.queryByRole("button", { name: "Save and exit" }),
).not.toBeInTheDocument();
});
@@ -255,7 +255,7 @@ describe("CreateFlowTopNav (viewport < sm2 / 440px)", () => {
).toBeInTheDocument();
expect(screen.getByRole("menuitem", { name: "Edit" })).toBeInTheDocument();
expect(
screen.getByRole("menuitem", { name: "Save & Exit" }),
screen.getByRole("menuitem", { name: "Save and exit" }),
).toBeInTheDocument();
});
@@ -284,7 +284,7 @@ describe("CreateFlowTopNav (viewport < sm2 / 440px)", () => {
expect(handleEdit).toHaveBeenCalledTimes(1);
await user.click(kebab);
await user.click(screen.getByRole("menuitem", { name: "Save & Exit" }));
await user.click(screen.getByRole("menuitem", { name: "Save and exit" }));
expect(handleExit).toHaveBeenCalledTimes(1);
});
});
+3 -3
View File
@@ -8,7 +8,7 @@ describe("InformationalScreen", () => {
render(<InformationalScreen />);
expect(
screen.getByRole("heading", {
name: "How CommunityRule helps groups like yours",
name: "Write your community’s Rule",
}),
).toBeInTheDocument();
});
@@ -17,7 +17,7 @@ describe("InformationalScreen", () => {
render(<InformationalScreen />);
expect(
screen.getByText(
/This flow will give you recommendations to improve your community/i,
/Work through it together or on your own/i,
),
).toBeInTheDocument();
});
@@ -30,7 +30,7 @@ describe("InformationalScreen", () => {
it("renders first numbered list item title", () => {
render(<InformationalScreen />);
expect(
screen.getByText("Tell us about your organization"),
screen.getByText("Tell us about your community"),
).toBeInTheDocument();
});
});
+23
View File
@@ -252,4 +252,27 @@ describe("MultiSelect – behaviour specifics", () => {
expect(handleClose).toHaveBeenCalled();
});
it("shows a danger hint under the chips when errorMessage is set", () => {
render(
<MultiSelect
options={defaultChipOptions}
errorMessage="Pick one to continue."
/>,
);
expect(screen.getByRole("alert")).toHaveTextContent(
"Pick one to continue.",
);
});
it("shows helper text beside the label", () => {
render(
<MultiSelect
label="Main kind"
helperText="Pick one"
options={defaultChipOptions}
/>,
);
expect(screen.getByText("Pick one")).toBeInTheDocument();
});
});
+3 -2
View File
@@ -61,8 +61,9 @@ describe("ProportionBar (behavioral tests)", () => {
it("handles all proportion states correctly", () => {
const testCases = [
{ progress: "1-0" as const, expected: 1 / 6 }, // First section 1/6 filled
{ progress: "1-5" as const, expected: 1 }, // First section 6/6 filled (fully filled)
{ progress: "1-0" as const, expected: 1 / 9 },
{ progress: "1-5" as const, expected: 6 / 9 },
{ progress: "1-8" as const, expected: 1 },
{ progress: "2-0" as const, expected: 1 }, // First section full, second empty
{ progress: "2-2" as const, expected: 1 + 1 / 2 }, // 1 + 1/2 per Figma `18861:15250`
{ progress: "2-3" as const, expected: 1 + 3 / 4 }, // 1 + 3/4 per Figma `21434:17632`
+30 -2
View File
@@ -22,6 +22,23 @@ function ReviewWithTitle({ title }: { title: string }) {
return <CommunityReviewScreen />;
}
function ReviewWithAnswers() {
const { state, updateState } = useCreateFlow();
const seededRef = React.useRef(false);
useEffect(() => {
if (seededRef.current) return;
seededRef.current = true;
updateState({
title: "Garden Club",
selectedOrganizationTypeIds: ["mutualAid"],
selectedCommunitySizeIds: ["thirteenToOneHundred"],
selectedMaturityIds: ["established"],
});
}, [updateState]);
if (state.title !== "Garden Club") return null;
return <CommunityReviewScreen />;
}
describe("CommunityReviewScreen", () => {
beforeEach(() => {
testRouter.replace.mockReset();
@@ -53,7 +70,7 @@ describe("CommunityReviewScreen", () => {
render(<ReviewWithTitle title="Garden Club" />);
expect(
screen.getByRole("heading", {
name: "Your community is added - congrats!",
name: "Here is your community",
}),
).toBeInTheDocument();
});
@@ -62,7 +79,7 @@ describe("CommunityReviewScreen", () => {
render(<ReviewWithTitle title="Garden Club" />);
expect(
screen.getByText(
/In the next section, we'll go through membership, decision-making, conflict resolution, and community values and create a custom operating manual for your organization based on the specifics you just shared./i,
/Next you will write your Rule/i,
),
).toBeInTheDocument();
});
@@ -81,6 +98,17 @@ describe("CommunityReviewScreen", () => {
).not.toBeInTheDocument();
});
it("lists kind, size, and maturity with Edit controls", () => {
render(<ReviewWithAnswers />);
expect(screen.getByText("Kind")).toBeInTheDocument();
expect(screen.getByText("Mutual aid")).toBeInTheDocument();
expect(screen.getByText("Size")).toBeInTheDocument();
expect(screen.getByText("13 to 100 people")).toBeInTheDocument();
expect(screen.getByText("Maturity")).toBeInTheDocument();
expect(screen.getByText("Established")).toBeInTheDocument();
expect(screen.getAllByRole("button", { name: "Edit" })).toHaveLength(3);
});
it("renders Rule as a button when a community name exists", () => {
render(<ReviewWithTitle title="Garden Club" />);
const buttons = screen.getAllByRole("button");
+13 -2
View File
@@ -31,7 +31,7 @@ describe("CreateFlowTextFieldScreen (community name)", () => {
/>,
);
expect(
screen.getByText("This will be the name of your community."),
screen.getByText("Use the name your members know. You can change this later."),
).toBeInTheDocument();
const input = screen.getByRole("textbox", { name: "Community name" });
expect(input).toBeInTheDocument();
@@ -46,18 +46,29 @@ describe("CreateFlowTextFieldScreen (community description)", () => {
<CreateFlowTextFieldScreen
messageNamespace="create.community.communityContext"
stateField="communityContext"
locationField="communityLocation"
maxLength={200}
multiline
/>,
);
expect(
screen.getByText(/Optional\. Write a short paragraph/i),
screen.getByText(
"For example, we cook and share free meals with neighbors every Sunday.",
),
).toBeInTheDocument();
const field = screen.getByRole("textbox", {
name: "Community description",
});
expect(field.tagName).toBe("TEXTAREA");
expect(field).not.toBeRequired();
const location = screen.getByRole("textbox", {
name: "Where is your community based?",
});
expect(location).toHaveAttribute("placeholder", "City or region");
expect(location).not.toBeRequired();
expect(
screen.getByText("Optional. A city or region is enough."),
).toBeInTheDocument();
});
});
+16
View File
@@ -109,4 +109,20 @@ describe("Upload (behavioral tests)", () => {
"text-[color:var(--color-content-default-tertiary)]",
);
});
it("shows a round preview, Replace, and filename when previewSrc is set", () => {
render(
<Upload
previewSrc="blob:logo"
previewAlt="Selected community logo preview"
fileName="logo.png"
buttonLabel="Replace"
/>,
);
expect(
screen.getByRole("img", { name: "Selected community logo preview" }),
).toHaveAttribute("src", "blob:logo");
expect(screen.getByRole("button", { name: "Replace" })).toBeInTheDocument();
expect(screen.getByText("logo.png")).toBeInTheDocument();
});
});
+9 -18
View File
@@ -5,7 +5,6 @@ import {
fireEvent,
cleanup,
} from "../utils/test-utils";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom/vitest";
import { CommunityUploadScreen } from "../../app/(app)/create/screens/upload/CommunityUploadScreen";
import {
@@ -70,7 +69,7 @@ describe("CommunityUploadScreen", () => {
render(<CommunityUploadScreen />);
expect(
screen.getByRole("heading", {
name: "Add a photo to identify your group",
name: "Add a logo",
}),
).toBeInTheDocument();
});
@@ -80,13 +79,12 @@ describe("CommunityUploadScreen", () => {
expect(screen.getByRole("button", { name: "Upload" })).toBeInTheDocument();
expect(
screen.getByText(
/This photo will be used as a profile picture for your group/i,
/Only people who can read your Rule can see this image/i,
),
).toBeInTheDocument();
});
it("restores a pending IndexedDB photo and lets the user remove it", async () => {
const user = userEvent.setup();
it("restores a pending IndexedDB photo into the uploaded preview", async () => {
const pending = new File([pngBytes], "avatar.png", { type: "image/png" });
vi.mocked(readPendingCommunityAvatarFile).mockResolvedValue(pending);
@@ -94,22 +92,15 @@ describe("CommunityUploadScreen", () => {
const preview = await screen.findByRole("img", { name: copy.previewAlt });
expect(preview).toHaveAttribute("src", "blob:pending-avatar");
const remove = screen.getByRole("button", {
name: copy.clearPendingUploadAriaLabel,
});
await user.click(remove);
expect(clearPendingCommunityAvatarFile).toHaveBeenCalled();
expect(screen.getByRole("button", { name: "Upload" })).toBeInTheDocument();
expect(
screen.queryByRole("img", { name: copy.previewAlt }),
).not.toBeInTheDocument();
screen.getByRole("button", { name: copy.replaceButton }),
).toBeInTheDocument();
expect(screen.getByText("avatar.png")).toBeInTheDocument();
});
it("rejects an empty file with a clear error and does not stage it", async () => {
render(<CommunityUploadScreen />);
await screen.findByText(copy.signInToUploadNote);
await screen.findByRole("heading", { name: copy.title });
fireEvent.change(fileInput(), {
target: { files: [new File([], "empty.png", { type: "image/png" })] },
@@ -123,7 +114,7 @@ describe("CommunityUploadScreen", () => {
it("rejects a text file renamed as PNG", async () => {
render(<CommunityUploadScreen />);
await screen.findByText(copy.signInToUploadNote);
await screen.findByRole("heading", { name: copy.title });
fireEvent.change(fileInput(), {
target: {
@@ -139,7 +130,7 @@ describe("CommunityUploadScreen", () => {
it("rejects SVG uploads", async () => {
render(<CommunityUploadScreen />);
await screen.findByText(copy.signInToUploadNote);
await screen.findByRole("heading", { name: copy.title });
fireEvent.change(fileInput(), {
target: {
+39 -1
View File
@@ -1,7 +1,11 @@
import { describe, expect, it } from "vitest";
import communitySize from "../../messages/en/create/community/communitySize.json";
import { SIZE_BANDS } from "../../lib/create/deckTypes";
import { isCommunitySizeStepBlocked } from "../../app/(app)/create/utils/createFlowFooterLabels";
import {
isCommunityNameStepBlocked,
isCommunitySizeStepBlocked,
isCommunityStructureStepBlocked,
} from "../../app/(app)/create/utils/createFlowFooterLabels";
/**
* Inclusive bounds. 100 stays in 13 to 100. More than 100 starts at 101.
@@ -64,3 +68,37 @@ describe("community size bands", () => {
).toBe(false);
});
});
describe("community name and type continue gates", () => {
it("blocks the name step until a title is entered", () => {
expect(isCommunityNameStepBlocked("community-name", undefined)).toBe(true);
expect(isCommunityNameStepBlocked("community-name", " ")).toBe(true);
expect(isCommunityNameStepBlocked("community-name", "Garden Club")).toBe(
false,
);
expect(isCommunityNameStepBlocked("community-size", "")).toBe(false);
});
it("blocks the type step until a main kind and a maturity are chosen", () => {
expect(
isCommunityStructureStepBlocked("community-structure", [], ["earlyStage"]),
).toBe(true);
expect(
isCommunityStructureStepBlocked(
"community-structure",
["mutualAid"],
[],
),
).toBe(true);
expect(
isCommunityStructureStepBlocked(
"community-structure",
["mutualAid"],
["established"],
),
).toBe(false);
expect(
isCommunityStructureStepBlocked("community-size", [], []),
).toBe(false);
});
});
@@ -17,11 +17,14 @@ describe("getProportionBarProgressForCreateFlowStep", () => {
);
});
it("uses 2-0 on community-save, review, and core-values (Create Community segment / same fill)", () => {
expect(getProportionBarProgressForCreateFlowStep("community-save")).toBe(
"2-0",
it("maps the rest of Create Community onto part 1, skipping setup at 1-5", () => {
expect(getProportionBarProgressForCreateFlowStep("community-upload")).toBe(
"1-6",
);
expect(getProportionBarProgressForCreateFlowStep("review")).toBe("2-0");
expect(getProportionBarProgressForCreateFlowStep("community-save")).toBe(
"1-7",
);
expect(getProportionBarProgressForCreateFlowStep("review")).toBe("1-8");
expect(getProportionBarProgressForCreateFlowStep("core-values")).toBe(
"2-0",
);
+6
View File
@@ -25,4 +25,10 @@ describe("create footer messages", () => {
"Publish CommunityRule",
);
});
it("exposes Skip and Skip for now for optional community steps", () => {
expect(messages.create.footer.skip).toBe("Skip");
expect(messages.create.footer.skipForNow).toBe("Skip for now");
expect(messages.create.footer.submitEmail).toBe("Submit email");
});
});
+9 -3
View File
@@ -175,7 +175,7 @@ describe("flowSteps", () => {
).toBeNull();
});
it("parseReviewReturnSearchParam accepts only final-review and edit-rule", () => {
it("parseReviewReturnSearchParam accepts final-review, edit-rule, and review", () => {
expect(
parseReviewReturnSearchParam(
new URLSearchParams("reviewReturn=final-review"),
@@ -186,16 +186,22 @@ describe("flowSteps", () => {
new URLSearchParams("reviewReturn=edit-rule"),
),
).toBe("edit-rule");
expect(
parseReviewReturnSearchParam(new URLSearchParams("reviewReturn=review")),
).toBe("review");
expect(
parseReviewReturnSearchParam(new URLSearchParams("reviewReturn=nope")),
).toBeNull();
expect(parseReviewReturnSearchParam(null)).toBeNull();
});
it("offers Save & Exit from community-structure through final-review and on edit-rule", () => {
it("offers Save and exit from community-save through final-review and on edit-rule", () => {
expect(shouldOfferCreateFlowSaveAndExit("informational")).toBe(false);
expect(shouldOfferCreateFlowSaveAndExit("community-name")).toBe(false);
expect(shouldOfferCreateFlowSaveAndExit("community-structure")).toBe(true);
expect(shouldOfferCreateFlowSaveAndExit("community-structure")).toBe(false);
expect(shouldOfferCreateFlowSaveAndExit("community-upload")).toBe(false);
expect(shouldOfferCreateFlowSaveAndExit("community-save")).toBe(true);
expect(shouldOfferCreateFlowSaveAndExit("review")).toBe(true);
expect(shouldOfferCreateFlowSaveAndExit("final-review")).toBe(true);
expect(shouldOfferCreateFlowSaveAndExit("edit-rule")).toBe(true);
});
@@ -18,4 +18,8 @@ describe("isValidCreateFlowSaveEmail", () => {
it("accepts a required type=email value", () => {
expect(isValidCreateFlowSaveEmail("user@example.com")).toBe(true);
});
it("rejects a domain with no dot", () => {
expect(isValidCreateFlowSaveEmail("maya@example")).toBe(false);
});
});