Ask the six optional setup questions after size so each tick can change suggested cards, keeping the answers on the draft and off the published Rule.

This commit is contained in:
adilallo committed 2026-11-05 20:26:07 -07:00
1 parent b4bfa157f8
commit bdaf1818d9
23 files changed
+399 -82

No files matched your search

@@ -91,6 +91,10 @@ import {
isCommunitySizeStepBlocked, isCommunitySizeStepBlocked,
isCommunityStructureStepBlocked, isCommunityStructureStepBlocked,
} from "./utils/createFlowFooterLabels"; } from "./utils/createFlowFooterLabels";
import {
EMPTY_SETUP_TRAITS,
hasTickedSetupTrait,
} from "../../../lib/create/setupTraits";
import { useAuthModal } from "../../contexts/AuthModalContext"; import { useAuthModal } from "../../contexts/AuthModalContext";
import { useMessages, useTranslation } from "../../contexts/MessagesContext"; import { useMessages, useTranslation } from "../../contexts/MessagesContext";
import { PostLoginDraftTransfer } from "./PostLoginDraftTransfer"; import { PostLoginDraftTransfer } from "./PostLoginDraftTransfer";
@@ -984,6 +988,37 @@ function CreateFlowLayoutContent({
: footer.submitEmail} : footer.submitEmail}
</Button> </Button>
</div> </div>
) : currentStep === "setup-questions" && nextStep ? (
<div className="flex flex-shrink-0 items-center gap-3 md:gap-4">
<Button
buttonType="filled"
palette="default"
size="xsmall"
disabled={
isPublishing || !hasTickedSetupTrait(state.setupTraits)
}
className={CREATE_FLOW_FOOTER_BUTTON_CLASS}
onClick={() => {
goToNextStep();
}}
>
{footer.continue}
</Button>
<Button
buttonType="outline"
palette="default"
size="xsmall"
disabled={isPublishing}
className={CREATE_FLOW_FOOTER_BUTTON_CLASS}
onClick={() => {
markCreateFlowInteraction();
updateState({ setupTraits: { ...EMPTY_SETUP_TRAITS } });
goToNextStep();
}}
>
{footer.skip}
</Button>
</div>
) : currentStep === "community-upload" && nextStep ? ( ) : currentStep === "community-upload" && nextStep ? (
<Button <Button
buttonType={hasCommunityLogo ? "filled" : "outline"} buttonType={hasCommunityLogo ? "filled" : "outline"}
@@ -27,6 +27,13 @@ const CommunitySizeSelectScreen = dynamic(
})), })),
{ loading: () => null }, { loading: () => null },
); );
const SetupQuestionsScreen = dynamic(
() =>
import("./select/SetupQuestionsScreen").then((m) => ({
default: m.SetupQuestionsScreen,
})),
{ loading: () => null },
);
const CommunityStructureSelectScreen = dynamic( const CommunityStructureSelectScreen = dynamic(
() => () =>
import("./select/CommunityStructureSelectScreen").then((m) => ({ import("./select/CommunityStructureSelectScreen").then((m) => ({
@@ -135,6 +142,8 @@ export function renderCreateFlowScreen(screenId: CreateFlowStep): ReactNode {
); );
case "community-size": case "community-size":
return <CommunitySizeSelectScreen />; return <CommunitySizeSelectScreen />;
case "setup-questions":
return <SetupQuestionsScreen />;
case "community-upload": case "community-upload":
return <CommunityUploadScreen />; return <CommunityUploadScreen />;
case "community-save": case "community-save":
@@ -0,0 +1,46 @@
"use client";
import CheckboxGroup from "../../../../components/controls/CheckboxGroup";
import { useMessages } from "../../../../contexts/MessagesContext";
import { useCreateFlow } from "../../context/CreateFlowContext";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
import {
setupTraitsFromTickedIds,
tickedSetupTraitIds,
} from "../../../../../lib/create/setupTraits";
/**
* Create Community — Figma Flow - Checkboxes (25224:105566).
*/
export function SetupQuestionsScreen() {
const copy = useMessages().create.community.setupQuestions;
const { markCreateFlowInteraction, updateState, state } = useCreateFlow();
return (
<CreateFlowTwoColumnSelectShell
header={
<CreateFlowHeaderLockup
title={copy.title}
description={copy.description}
justification="left"
/>
}
>
<CheckboxGroup
value={tickedSetupTraitIds(state.setupTraits)}
onChange={({ value }) => {
markCreateFlowInteraction();
updateState({ setupTraits: setupTraitsFromTickedIds(value) });
}}
options={copy.questions.map((question) => ({
value: question.id,
label: question.label,
subtext: question.help,
}))}
aria-label={copy.title}
className="flex w-full flex-col gap-[20px] !space-y-0"
/>
</CreateFlowTwoColumnSelectShell>
);
}
+1
View File
@@ -16,6 +16,7 @@ export type CreateFlowStep =
| "informational" | "informational"
| "community-name" | "community-name"
| "community-size" | "community-size"
| "setup-questions"
| "community-context" | "community-context"
| "community-structure" | "community-structure"
| "community-upload" | "community-upload"
@@ -12,7 +12,8 @@ const PROPORTION_BY_STEP_INDEX: readonly ProportionBarState[] = [
"1-2", // community-structure "1-2", // community-structure
"1-3", // community-context "1-3", // community-context
"1-4", // community-size "1-4", // community-size
"1-6", // community-upload (1-5 is reserved for setup questions) "1-5", // setup-questions
"1-6", // community-upload
"1-7", // community-save "1-7", // community-save
"1-8", // review "1-8", // review
"2-0", // core-values (same fill as 1-8) "2-0", // core-values (same fill as 1-8)
@@ -57,6 +57,12 @@ export const CREATE_FLOW_SCREEN_REGISTRY: Record<
messageNamespace: "create.community.communitySize", messageNamespace: "create.community.communitySize",
centeredBodyBelowMd: false, centeredBodyBelowMd: false,
}, },
"setup-questions": {
layoutKind: "select",
figmaNodeId: "25224-105566",
messageNamespace: "create.community.setupQuestions",
centeredBodyBelowMd: false,
},
"community-context": { "community-context": {
layoutKind: "text", layoutKind: "text",
figmaNodeId: "25224-106030", figmaNodeId: "25224-106030",
+7 -3
View File
@@ -2,8 +2,9 @@
* Step definitions and helpers for the Create Rule Flow * Step definitions and helpers for the Create Rule Flow
* *
* Single source of truth for step order and navigation helpers. * Single source of truth for step order and navigation helpers.
* Order matches Figma Create Community (frames 1–8) then later stages. * Order matches Figma Create Community then later stages.
* `community-structure` precedes `community-context` and `community-size` (Figma frame 3 vs 5 swap). * `community-structure` precedes `community-context` and `community-size` (Figma frame 3 vs 5 swap).
* `setup-questions` sits after size and before the logo upload.
*/ */
import type { CreateFlowStep } from "../types"; import type { CreateFlowStep } from "../types";
@@ -17,6 +18,7 @@ export const FLOW_STEP_ORDER: readonly CreateFlowStep[] = [
"community-structure", "community-structure",
"community-context", "community-context",
"community-size", "community-size",
"setup-questions",
"community-upload", "community-upload",
"community-save", "community-save",
"review", "review",
@@ -49,8 +51,8 @@ export type CreateFlowNavigationOptions = {
skipCommunitySave?: boolean; skipCommunitySave?: boolean;
/** /**
* Template **Use without changes**: collect community name, description, and * Template **Use without changes**: collect community name, description, and
* optional photo only. Skips intro, structure, size, save, community review, * optional photo only. Skips intro, structure, size, setup questions, save,
* and the custom-rule authoring segment. * community review, and the custom-rule authoring segment.
*/ */
useWithoutChangesIdentityOnly?: boolean; useWithoutChangesIdentityOnly?: boolean;
}; };
@@ -65,6 +67,7 @@ const USE_WITHOUT_CHANGES_IDENTITY_SKIP = new Set<CreateFlowStep>([
"informational", "informational",
"community-structure", "community-structure",
"community-size", "community-size",
"setup-questions",
"community-save", "community-save",
"review", "review",
"core-values", "core-values",
@@ -199,6 +202,7 @@ export function shouldOfferCreateFlowSaveAndExit(
*/ */
export const CREATE_FLOW_SELECT_SPLIT_SCROLL_STEPS: readonly CreateFlowStep[] = [ export const CREATE_FLOW_SELECT_SPLIT_SCROLL_STEPS: readonly CreateFlowStep[] = [
"community-size", "community-size",
"setup-questions",
"community-structure", "community-structure",
"core-values", "core-values",
"communication-methods", "communication-methods",
@@ -35,9 +35,14 @@ export function CheckboxGroupView({
onOptionChange(option.value, checked); onOptionChange(option.value, checked);
}} }}
/> />
<div className="flex flex-col gap-[4px] flex-1"> <div
className="flex flex-col gap-[4px] flex-1 min-w-0 cursor-pointer"
onClick={() => {
if (!disabled) onOptionChange(option.value, !isChecked);
}}
>
<span <span
className={`text-small-paragraph ${ className={`text-paragraph-m ${
mode === "inverse" mode === "inverse"
? "text-[var(--color-content-invert-primary)]" ? "text-[var(--color-content-invert-primary)]"
: "text-[var(--color-content-default-primary)]" : "text-[var(--color-content-default-primary)]"
+15 -14
View File
@@ -6,11 +6,11 @@ Product/engineering reference for the **custom** “Create rule” experience: U
## Product stages (Figma) ## Product stages (Figma)
The Figma **Create Community** sequence is the **source of truth** for the first segment of the wizard (eight frames). After **`review`**, the flow continues with **Create Custom CommunityRule** and **Review and complete** stages. The shipped URL sequence in [`FLOW_STEP_ORDER`](../app/(app)/create/utils/flowSteps.ts) **follows that trajectory**; stages are a **product** slice of that linear order, not separate routers today. The Figma **Create Community** sequence is the **source of truth** for the first segment of the wizard. After **`review`**, the flow continues with **Create Custom CommunityRule** and **Review and complete** stages. The shipped URL sequence in [`FLOW_STEP_ORDER`](../app/(app)/create/utils/flowSteps.ts) **follows that trajectory**; stages are a **product** slice of that linear order, not separate routers today.
| Stage (Figma) | Purpose (summary) | `CreateFlowStep` values (in order) | | Stage (Figma) | Purpose (summary) | `CreateFlowStep` values (in order) |
| --- | --- | --- | | --- | --- | --- |
| **Create Community** | Intro, naming, structure, context, size, upload, save progress (email), then community review. | `informational` → `community-name` → `community-structure` → `community-context` → `community-size` → `community-upload` → `community-save` → `review` | | **Create Community** | Intro, naming, structure, context, size, optional setup questions, upload, save progress (email), then community review. | `informational` → `community-name` → `community-structure` → `community-context` → `community-size` → `setup-questions` → `community-upload` → `community-save` → `review` |
| **Create Custom CommunityRule** | Author the CommunityRule content and structure (core values + four card-stack categories). | `core-values` → `communication-methods` → `membership-methods` → `decision-approaches` → `conflict-management` | | **Create Custom CommunityRule** | Author the CommunityRule content and structure (core values + four card-stack categories). | `core-values` → `communication-methods` → `membership-methods` → `decision-approaches` → `conflict-management` |
| **Review and complete** | Stakeholders, final card, publish, success. | `confirm-stakeholders` → `final-review` → `completed` | | **Review and complete** | Stakeholders, final card, publish, success. | `confirm-stakeholders` → `final-review` → `completed` |
@@ -31,17 +31,18 @@ Order is defined in code by [`FLOW_STEP_ORDER`](../app/(app)/create/utils/flowSt
| 3 | Create Community | `community-structure` | `/create/community-structure` | | 3 | Create Community | `community-structure` | `/create/community-structure` |
| 4 | Create Community | `community-context` | `/create/community-context` | | 4 | Create Community | `community-context` | `/create/community-context` |
| 5 | Create Community | `community-size` | `/create/community-size` | | 5 | Create Community | `community-size` | `/create/community-size` |
| 6 | Create Community | `community-upload` | `/create/community-upload` | | 6 | Create Community | `setup-questions` | `/create/setup-questions` |
| 7 | Create Community | `community-save` | `/create/community-save` | | 7 | Create Community | `community-upload` | `/create/community-upload` |
| 8 | Create Community (review frame) | `review` | `/create/review` | | 8 | Create Community | `community-save` | `/create/community-save` |
| 9 | Create Custom CommunityRule | `core-values` | `/create/core-values` | | 9 | Create Community (review frame) | `review` | `/create/review` |
| 10 | Create Custom CommunityRule | `communication-methods` | `/create/communication-methods` | | 10 | Create Custom CommunityRule | `core-values` | `/create/core-values` |
| 11 | Create Custom CommunityRule | `membership-methods` | `/create/membership-methods` | | 11 | Create Custom CommunityRule | `communication-methods` | `/create/communication-methods` |
| 12 | Create Custom CommunityRule | `decision-approaches` | `/create/decision-approaches` | | 12 | Create Custom CommunityRule | `membership-methods` | `/create/membership-methods` |
| 13 | Create Custom CommunityRule | `conflict-management` | `/create/conflict-management` | | 13 | Create Custom CommunityRule | `decision-approaches` | `/create/decision-approaches` |
| 14 | Review and complete | `confirm-stakeholders` | `/create/confirm-stakeholders` | | 14 | Create Custom CommunityRule | `conflict-management` | `/create/conflict-management` |
| 15 | Review and complete | `final-review` | `/create/final-review` | | 15 | Review and complete | `confirm-stakeholders` | `/create/confirm-stakeholders` |
| 16 | Review and complete | `completed` | `/create/completed` | | 16 | Review and complete | `final-review` | `/create/final-review` |
| 17 | Review and complete | `completed` | `/create/completed` |
**Primary entry:** marketing header **Create rule** and profile **Create new custom Rule** both run **`prepareFreshCreateFlowEntry`** then navigate to **`/create`**, which redirects to **`/create/informational`** (see [`Top.container.tsx`](../app/components/navigation/Top/Top.container.tsx) and [`ProfilePageClient.tsx`](../app/(app)/profile/ProfilePageClient.tsx)). **Primary entry:** marketing header **Create rule** and profile **Create new custom Rule** both run **`prepareFreshCreateFlowEntry`** then navigate to **`/create`**, which redirects to **`/create/informational`** (see [`Top.container.tsx`](../app/components/navigation/Top/Top.container.tsx) and [`ProfilePageClient.tsx`](../app/(app)/profile/ProfilePageClient.tsx)).
@@ -87,7 +88,7 @@ From that page, **Customize** pre-fills the custom-rule selections on the curren
**Entering a template before community stage is done.** Both handlers apply their side effects eagerly (prefill for Customize; `sections` for Use without changes) *and* pin a `pendingTemplateAction: { slug, mode }` when the community questionnaire still needs to run. **Entering a template before community stage is done.** Both handlers apply their side effects eagerly (prefill for Customize; `sections` for Use without changes) *and* pin a `pendingTemplateAction: { slug, mode }` when the community questionnaire still needs to run.
- **Customize** routes to `/create/informational` and walks the full Create Community questionnaire. Once the user reaches `/create/review`, [`CommunityReviewScreen`](../app/(app)/create/screens/review/CommunityReviewScreen.tsx) reads the action on mount, clears it via `updateState`, and `router.replace`s to `/create/core-values`. `replace` (not `push`) keeps `community-save` as the Back-button target from that destination. - **Customize** routes to `/create/informational` and walks the full Create Community questionnaire. Once the user reaches `/create/review`, [`CommunityReviewScreen`](../app/(app)/create/screens/review/CommunityReviewScreen.tsx) reads the action on mount, clears it via `updateState`, and `router.replace`s to `/create/core-values`. `replace` (not `push`) keeps `community-save` as the Back-button target from that destination.
- **Use without changes (direct from a template)** — home Popular templates, `/templates` without `fromFlow`, or a pasted review-template URL. Always routes to `/create/community-name` and walks **name → description → optional photo**, then `/create/confirm-stakeholders`. A leftover draft title is **not** “community already done”; those identity fields are cleared so cache cannot skip or pre-fill the path. Intro, structure, size, save, community review, and custom-rule authoring are skipped. Back from name returns to template review. - **Use without changes (direct from a template)** — home Popular templates, `/templates` without `fromFlow`, or a pasted review-template URL. Always routes to `/create/community-name` and walks **name → description → optional photo**, then `/create/confirm-stakeholders`. A leftover draft title is **not** “community already done”; those identity fields are cleared so cache cannot skip or pre-fill the path. Intro, structure, size, setup questions, save, community review, and custom-rule authoring are skipped. Back from name returns to template review.
- **Use without changes (in-flow)** — `/create/review` → `/create/templates` → template. Community identity is already collected, so this action seeds the template and routes to `/create/confirm-stakeholders`. Back returns to template review. - **Use without changes (in-flow)** — `/create/review` → `/create/templates` → template. Community identity is already collected, so this action seeds the template and routes to `/create/confirm-stakeholders`. Back returns to template review.
The action for Customize is cleared on the first fire so later direct visits to `/create/review` render normally. The review screen is not part of the Use-without-changes identity path. If they land on `/create/review` with a leftover Use-without-changes pin, the screen `replace`s to `community-name`. The action for Customize is cleared on the first fire so later direct visits to `/create/review` render normally. The review screen is not part of the Use-without-changes identity path. If they land on `/create/review` with a leftover Use-without-changes pin, the screen `replace`s to `community-name`.
+35
View File
@@ -0,0 +1,35 @@
import type { CreateFlowState } from "../../app/(app)/create/types";
import { TRAIT_KEYS, type TraitKey } from "./deckTypes";
export const EMPTY_SETUP_TRAITS: Record<TraitKey, boolean> = {
network: false,
nonvoters: false,
employer: false,
sponsor: false,
records: false,
risky: false,
};
export function tickedSetupTraitIds(
traits: CreateFlowState["setupTraits"],
): TraitKey[] {
if (!traits) return [];
return TRAIT_KEYS.filter((key) => traits[key] === true);
}
export function setupTraitsFromTickedIds(
ids: readonly string[],
): Record<TraitKey, boolean> {
const ticked = new Set(ids);
const out = { ...EMPTY_SETUP_TRAITS };
for (const key of TRAIT_KEYS) {
out[key] = ticked.has(key);
}
return out;
}
export function hasTickedSetupTrait(
traits: CreateFlowState["setupTraits"],
): boolean {
return tickedSetupTraitIds(traits).length > 0;
}
@@ -0,0 +1,37 @@
{
"_comment": "Create Community setup questions (Figma Flow - Checkboxes 25224:105566). Copy from Figma at build time. Question ids are the trait keys the decks read.",
"title": "A few more things about your community",
"description": "Tick any that are true. Each one adds or changes a suggested card. We keep your answers with your draft and never publish them.",
"questions": [
{
"id": "network",
"label": "Some of our members or partners are organizations",
"help": "Such as crews in a network, organizations in a coalition or businesses in a co-op"
},
{
"id": "nonvoters",
"label": "Some people who do our work do not vote",
"help": "Such as volunteers who are not members, staff who are not owners or contractors"
},
{
"id": "employer",
"label": "We pay some people to do our work",
"help": "Wages or salary, including worker-owners on payroll. Leave it unticked if everyone is a volunteer or a contractor"
},
{
"id": "sponsor",
"label": "Another organization holds our money or legal status",
"help": "Such as a fiscal sponsor, a parent nonprofit or a church that takes donations for us"
},
{
"id": "records",
"label": "We keep information about the people we serve",
"help": "Such as names, addresses, needs, health details or intake notes. A list of our own members does not count"
},
{
"id": "risky",
"label": "Our work carries physical risk, or needs a license, permit or inspection",
"help": "Such as medical care, building, driving, cooking for the public or caring for children"
}
]
}
+1
View File
@@ -1,5 +1,6 @@
{ {
"next": "Next", "next": "Next",
"continue": "Continue",
"skip": "Skip", "skip": "Skip",
"skipForNow": "Skip for now", "skipForNow": "Skip for now",
"saveLater": "Skip for now", "saveLater": "Skip for now",
-60
View File
@@ -1,60 +0,0 @@
{
"_comment": "Create flow, the setup questions screen after the four setup questions (Figma CR-291, frame 24715:27505) and the setup copy the real-group review changed. Copy agreed 24 Oct. The question ids are the trait keys in _jobs.json.",
"title": "A few more things about your group",
"description": "Tick any that are true. Each one adds or changes a suggested card. We keep your answers with your draft and never publish them.",
"continueButton": "Continue",
"skipButton": "Skip",
"questions": [
{
"id": "network",
"label": "Some of our members or partners are groups",
"help": "Such as crews in a network, organizations in a coalition or businesses in a co-op"
},
{
"id": "nonvoters",
"label": "Some people who do our work do not vote",
"help": "Such as volunteers who are not members, staff who are not owners or contractors"
},
{
"id": "employer",
"label": "We pay some people to do our work",
"help": "Wages or salary, including worker-owners on payroll. Leave it unticked if everyone is a volunteer or a contractor"
},
{
"id": "sponsor",
"label": "Another organization holds our money or legal status",
"help": "Such as a fiscal sponsor, a parent nonprofit or a church that takes donations for us"
},
{
"id": "records",
"label": "We keep information about the people we serve",
"help": "Such as names, addresses, needs, health details or intake notes. A list of our own members does not count"
},
{
"id": "risky",
"label": "Our work carries physical risk, or needs a license, permit or inspection",
"help": "Such as medical care, building, driving, cooking for the public or caring for children"
}
],
"sizeHelp": "Count the people who have a say in your big decisions. If others take part without a vote, you can say so later in setup.",
"kind": {
"title": "What kind of group are you, mainly?",
"hint": "Pick one.",
"othersTitle": "Anything else that fits?",
"othersHelp": "These help us choose between cards and never add cards on their own."
},
"supermajorityShare": {
"_comment": "Draft copy, waits on Dani’s sign-off. The question itself was agreed 27 Oct, calls 3 and 14. Nothing is preselected. A group that starts from a template answers it at final review.",
"title": "What share of your members must agree to your biggest decisions?",
"help": "This is your supermajority share. Your Rule uses it for votes such as changing the Rule, ending a membership and emergencies. Cards that set their own share keep it.",
"options": {
"twoThirds": "Two thirds",
"threeQuarters": "Three quarters",
"everyone": "Everyone",
"custom": "Another share"
},
"customLabel": "Share of active members who vote, as a percent",
"warnEveryone": "With everyone, one member can block a change to your Rule or a way out of a deadlock.",
"warnHalfOrBelow": "At half or below, a change to your Rule could pass without most of the members who vote."
}
}
+2
View File
@@ -46,6 +46,7 @@ import createCommunityName from "./create/community/communityName.json";
import createCommunityStructure from "./create/community/communityStructure.json"; import createCommunityStructure from "./create/community/communityStructure.json";
import createCommunityContext from "./create/community/communityContext.json"; import createCommunityContext from "./create/community/communityContext.json";
import createCommunitySize from "./create/community/communitySize.json"; import createCommunitySize from "./create/community/communitySize.json";
import createSetupQuestions from "./create/community/setupQuestions.json";
import createCommunityUpload from "./create/community/communityUpload.json"; import createCommunityUpload from "./create/community/communityUpload.json";
import createCommunitySave from "./create/community/communitySave.json"; import createCommunitySave from "./create/community/communitySave.json";
import createReview from "./create/community/review.json"; import createReview from "./create/community/review.json";
@@ -119,6 +120,7 @@ export default {
communityStructure: createCommunityStructure, communityStructure: createCommunityStructure,
communityContext: createCommunityContext, communityContext: createCommunityContext,
communitySize: createCommunitySize, communitySize: createCommunitySize,
setupQuestions: createSetupQuestions,
communityUpload: createCommunityUpload, communityUpload: createCommunityUpload,
communitySave: createCommunitySave, communitySave: createCommunitySave,
review: createReview, review: createReview,
+90
View File
@@ -0,0 +1,90 @@
import "@testing-library/jest-dom/vitest";
import { useLayoutEffect } from "react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it } from "vitest";
import { SetupQuestionsScreen } from "../../app/(app)/create/screens/select/SetupQuestionsScreen";
import { useCreateFlow } from "../../app/(app)/create/context/CreateFlowContext";
import type { CreateFlowState } from "../../app/(app)/create/types";
import { TRAIT_KEYS } from "../../lib/create/deckTypes";
import messages from "../../messages/en/index";
import {
cleanup,
renderWithProviders as render,
screen,
} from "../utils/test-utils";
const copy = messages.create.community.setupQuestions;
function ScreenWithState({ initial }: { initial: CreateFlowState }) {
const { replaceState } = useCreateFlow();
useLayoutEffect(() => {
replaceState(initial);
}, [replaceState, initial]);
return <SetupQuestionsScreen />;
}
afterEach(() => {
cleanup();
});
describe("Create flow setup-questions page", () => {
it("renders the six questions off by default with a help line under each", () => {
render(<SetupQuestionsScreen />);
expect(
screen.getByRole("heading", { name: copy.title }),
).toBeInTheDocument();
expect(screen.getByText(copy.description)).toBeInTheDocument();
for (const question of copy.questions) {
const checkbox = screen.getByRole("checkbox", { name: question.label });
expect(checkbox).toHaveAttribute("aria-checked", "false");
expect(screen.getByText(question.help)).toBeInTheDocument();
}
expect(copy.questions).toHaveLength(6);
expect(copy.questions.map((question) => question.id)).toEqual([
...TRAIT_KEYS,
]);
});
it("ticks a question from the box or the help line", async () => {
const user = userEvent.setup();
render(<SetupQuestionsScreen />);
await user.click(
screen.getByRole("checkbox", { name: copy.questions[2].label }),
);
expect(
screen.getByRole("checkbox", { name: copy.questions[2].label }),
).toHaveAttribute("aria-checked", "true");
await user.click(screen.getByText(copy.questions[0].help));
expect(
screen.getByRole("checkbox", { name: copy.questions[0].label }),
).toHaveAttribute("aria-checked", "true");
});
it("restores ticked answers from the draft", () => {
render(
<ScreenWithState
initial={{ setupTraits: { employer: true, sponsor: false } }}
/>,
);
expect(
screen.getByRole("checkbox", { name: copy.questions[2].label }),
).toHaveAttribute("aria-checked", "true");
expect(
screen.getByRole("checkbox", { name: copy.questions[3].label }),
).toHaveAttribute("aria-checked", "false");
});
it("does not ask about police, immigration enforcement, or protest", () => {
const blob = `${copy.title} ${copy.description} ${copy.questions
.map((question) => `${question.label} ${question.help}`)
.join(" ")}`.toLowerCase();
expect(blob).not.toMatch(/police/);
expect(blob).not.toMatch(/immigration/);
expect(blob).not.toMatch(/protest/);
});
});
+11
View File
@@ -22,6 +22,17 @@ describe("buildPublishPayload", () => {
}); });
}); });
it("omits setup answers from the published Rule", () => {
const r = buildPublishPayload({
title: "Oak Park Commons",
setupTraits: { network: true, employer: true, risky: false },
});
expect(r.ok).toBe(true);
if (!r.ok) return;
expect(r.document).not.toHaveProperty("setupTraits");
expect(JSON.stringify(r)).not.toContain("setupTraits");
});
it("returns title and fallback Overview section when no sections", () => { it("returns title and fallback Overview section when no sections", () => {
const r = buildPublishPayload({ title: "Oak Park Commons" }); const r = buildPublishPayload({ title: "Oak Park Commons" });
expect(r.ok).toBe(true); expect(r.ok).toBe(true);
@@ -58,6 +58,9 @@ describe("createFlowLayoutTokens", () => {
expect(getCreateFlowContentMaxClass({ step: "community-structure" })).toBe( expect(getCreateFlowContentMaxClass({ step: "community-structure" })).toBe(
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS, CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
); );
expect(getCreateFlowContentMaxClass({ step: "setup-questions" })).toBe(
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
);
expect( expect(
getCreateFlowContentMaxClass({ step: "communication-methods" }), getCreateFlowContentMaxClass({ step: "communication-methods" }),
).toBe(CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS); ).toBe(CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS);
@@ -17,7 +17,10 @@ describe("getProportionBarProgressForCreateFlowStep", () => {
); );
}); });
it("maps the rest of Create Community onto part 1, skipping setup at 1-5", () => { it("maps the rest of Create Community onto part 1, with setup questions at 1-5", () => {
expect(getProportionBarProgressForCreateFlowStep("setup-questions")).toBe(
"1-5",
);
expect(getProportionBarProgressForCreateFlowStep("community-upload")).toBe( expect(getProportionBarProgressForCreateFlowStep("community-upload")).toBe(
"1-6", "1-6",
); );
+22
View File
@@ -69,6 +69,28 @@ describe("createFlowStateSchema", () => {
expect(r.success).toBe(false); expect(r.success).toBe(false);
}); });
it("accepts setup-questions as currentStep and true or false setupTraits", () => {
const r = createFlowStateSchema.safeParse({
currentStep: "setup-questions",
setupTraits: {
network: true,
nonvoters: false,
employer: true,
sponsor: false,
records: false,
risky: true,
},
});
expect(r.success).toBe(true);
});
it("rejects non-boolean setupTraits values", () => {
const r = createFlowStateSchema.safeParse({
setupTraits: { network: "yes" },
});
expect(r.success).toBe(false);
});
it("rejects title that is too long", () => { it("rejects title that is too long", () => {
const r = createFlowStateSchema.safeParse({ title: "x".repeat(600) }); const r = createFlowStateSchema.safeParse({ title: "x".repeat(600) });
expect(r.success).toBe(false); expect(r.success).toBe(false);
+5
View File
@@ -26,6 +26,11 @@ describe("create footer messages", () => {
); );
}); });
it("exposes Continue and Skip for the setup questions step", () => {
expect(messages.create.footer.continue).toBe("Continue");
expect(messages.create.footer.skip).toBe("Skip");
});
it("exposes Skip and Skip for now for optional community steps", () => { it("exposes Skip and Skip for now for optional community steps", () => {
expect(messages.create.footer.skip).toBe("Skip"); expect(messages.create.footer.skip).toBe("Skip");
expect(messages.create.footer.skipForNow).toBe("Skip for now"); expect(messages.create.footer.skipForNow).toBe("Skip for now");
+14 -1
View File
@@ -69,6 +69,14 @@ describe("flowSteps", () => {
expect(getNextStep("community-context")).toBe("community-size"); expect(getNextStep("community-context")).toBe("community-size");
}); });
it("places setup-questions after community-size and before the logo upload", () => {
expect(getStepIndex("setup-questions")).toBe(5);
expect(getNextStep("community-size")).toBe("setup-questions");
expect(getNextStep("setup-questions")).toBe("community-upload");
expect(getPreviousStep("community-upload")).toBe("setup-questions");
expect(getPreviousStep("setup-questions")).toBe("community-size");
});
it("skipCommunitySave bridges upload → review and review → upload", () => { it("skipCommunitySave bridges upload → review and review → upload", () => {
const opts = { skipCommunitySave: true } as const; const opts = { skipCommunitySave: true } as const;
expect(getNextStep("community-upload", opts)).toBe("review"); expect(getNextStep("community-upload", opts)).toBe("review");
@@ -77,7 +85,8 @@ describe("flowSteps", () => {
it("skipCommunitySave does not change steps outside the save segment", () => { it("skipCommunitySave does not change steps outside the save segment", () => {
const opts = { skipCommunitySave: true } as const; const opts = { skipCommunitySave: true } as const;
expect(getNextStep("community-size", opts)).toBe("community-upload"); expect(getNextStep("community-size", opts)).toBe("setup-questions");
expect(getNextStep("setup-questions", opts)).toBe("community-upload");
expect(getNextStep("review", opts)).toBe("core-values"); expect(getNextStep("review", opts)).toBe("core-values");
expect(getPreviousStep("communication-methods", opts)).toBe("core-values"); expect(getPreviousStep("communication-methods", opts)).toBe("core-values");
}); });
@@ -86,6 +95,8 @@ describe("flowSteps", () => {
const opts = { useWithoutChangesIdentityOnly: true } as const; const opts = { useWithoutChangesIdentityOnly: true } as const;
expect(getNextStep("community-name", opts)).toBe("community-context"); expect(getNextStep("community-name", opts)).toBe("community-context");
expect(getNextStep("community-context", opts)).toBe("community-upload"); expect(getNextStep("community-context", opts)).toBe("community-upload");
expect(getNextStep("community-size", opts)).toBe("community-upload");
expect(getNextStep("setup-questions", opts)).toBe("community-upload");
expect(getNextStep("community-upload", opts)).toBe("confirm-stakeholders"); expect(getNextStep("community-upload", opts)).toBe("confirm-stakeholders");
expect(getNextStep("confirm-stakeholders", opts)).toBe("final-review"); expect(getNextStep("confirm-stakeholders", opts)).toBe("final-review");
expect(getPreviousStep("community-context", opts)).toBe("community-name"); expect(getPreviousStep("community-context", opts)).toBe("community-name");
@@ -199,6 +210,7 @@ describe("flowSteps", () => {
expect(shouldOfferCreateFlowSaveAndExit("informational")).toBe(false); expect(shouldOfferCreateFlowSaveAndExit("informational")).toBe(false);
expect(shouldOfferCreateFlowSaveAndExit("community-name")).toBe(false); expect(shouldOfferCreateFlowSaveAndExit("community-name")).toBe(false);
expect(shouldOfferCreateFlowSaveAndExit("community-structure")).toBe(false); expect(shouldOfferCreateFlowSaveAndExit("community-structure")).toBe(false);
expect(shouldOfferCreateFlowSaveAndExit("setup-questions")).toBe(false);
expect(shouldOfferCreateFlowSaveAndExit("community-upload")).toBe(false); expect(shouldOfferCreateFlowSaveAndExit("community-upload")).toBe(false);
expect(shouldOfferCreateFlowSaveAndExit("community-save")).toBe(true); expect(shouldOfferCreateFlowSaveAndExit("community-save")).toBe(true);
expect(shouldOfferCreateFlowSaveAndExit("review")).toBe(true); expect(shouldOfferCreateFlowSaveAndExit("review")).toBe(true);
@@ -244,6 +256,7 @@ describe("flowSteps", () => {
expect(createFlowStepUsesSelectSplitScroll("community-structure")).toBe( expect(createFlowStepUsesSelectSplitScroll("community-structure")).toBe(
true, true,
); );
expect(createFlowStepUsesSelectSplitScroll("setup-questions")).toBe(true);
expect(createFlowStepUsesSelectSplitScroll("communication-methods")).toBe( expect(createFlowStepUsesSelectSplitScroll("communication-methods")).toBe(
true, true,
); );
+7
View File
@@ -19,6 +19,13 @@ describe("maturity and chip aliases", () => {
expect(answers.mainKind).toBe("workersCoop"); expect(answers.mainKind).toBe("workersCoop");
}); });
it("passes ticked setup answers through as traits", () => {
const answers = groupAnswersFromCreateFlowState({
setupTraits: { employer: true, records: false },
});
expect(answers.traits).toEqual({ employer: true, records: false });
});
it("loads Enterprise and Larger organization as Established", () => { it("loads Enterprise and Larger organization as Established", () => {
const fromId = migrateLegacyCreateFlowState({ const fromId = migrateLegacyCreateFlowState({
selectedMaturityIds: ["4"], selectedMaturityIds: ["4"],
+40
View File
@@ -0,0 +1,40 @@
import { describe, expect, it } from "vitest";
import { TRAIT_KEYS } from "../../lib/create/deckTypes";
import {
EMPTY_SETUP_TRAITS,
hasTickedSetupTrait,
setupTraitsFromTickedIds,
tickedSetupTraitIds,
} from "../../lib/create/setupTraits";
describe("setupTraits", () => {
it("starts with every question off", () => {
expect(TRAIT_KEYS.every((key) => EMPTY_SETUP_TRAITS[key] === false)).toBe(
true,
);
expect(hasTickedSetupTrait(undefined)).toBe(false);
expect(hasTickedSetupTrait(EMPTY_SETUP_TRAITS)).toBe(false);
expect(tickedSetupTraitIds(undefined)).toEqual([]);
});
it("round-trips ticked ids as true or false on every trait", () => {
const traits = setupTraitsFromTickedIds(["employer", "records"]);
expect(traits).toEqual({
network: false,
nonvoters: false,
employer: true,
sponsor: false,
records: true,
risky: false,
});
expect(tickedSetupTraitIds(traits)).toEqual(["employer", "records"]);
expect(hasTickedSetupTrait(traits)).toBe(true);
});
it("ignores ids that are not setup questions", () => {
expect(setupTraitsFromTickedIds(["employer", "police"])).toEqual({
...EMPTY_SETUP_TRAITS,
employer: true,
});
});
});