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:
1 parent
b4bfa157f8
commit
bdaf1818d9
23 files changed
+399
-82
No files matched your search
@@ -91,6 +91,10 @@ import {
|
||||
isCommunitySizeStepBlocked,
|
||||
isCommunityStructureStepBlocked,
|
||||
} from "./utils/createFlowFooterLabels";
|
||||
import {
|
||||
EMPTY_SETUP_TRAITS,
|
||||
hasTickedSetupTrait,
|
||||
} from "../../../lib/create/setupTraits";
|
||||
import { useAuthModal } from "../../contexts/AuthModalContext";
|
||||
import { useMessages, useTranslation } from "../../contexts/MessagesContext";
|
||||
import { PostLoginDraftTransfer } from "./PostLoginDraftTransfer";
|
||||
@@ -984,6 +988,37 @@ function CreateFlowLayoutContent({
|
||||
: footer.submitEmail}
|
||||
</Button>
|
||||
</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 ? (
|
||||
<Button
|
||||
buttonType={hasCommunityLogo ? "filled" : "outline"}
|
||||
|
||||
@@ -27,6 +27,13 @@ const CommunitySizeSelectScreen = dynamic(
|
||||
})),
|
||||
{ loading: () => null },
|
||||
);
|
||||
const SetupQuestionsScreen = dynamic(
|
||||
() =>
|
||||
import("./select/SetupQuestionsScreen").then((m) => ({
|
||||
default: m.SetupQuestionsScreen,
|
||||
})),
|
||||
{ loading: () => null },
|
||||
);
|
||||
const CommunityStructureSelectScreen = dynamic(
|
||||
() =>
|
||||
import("./select/CommunityStructureSelectScreen").then((m) => ({
|
||||
@@ -135,6 +142,8 @@ export function renderCreateFlowScreen(screenId: CreateFlowStep): ReactNode {
|
||||
);
|
||||
case "community-size":
|
||||
return <CommunitySizeSelectScreen />;
|
||||
case "setup-questions":
|
||||
return <SetupQuestionsScreen />;
|
||||
case "community-upload":
|
||||
return <CommunityUploadScreen />;
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ export type CreateFlowStep =
|
||||
| "informational"
|
||||
| "community-name"
|
||||
| "community-size"
|
||||
| "setup-questions"
|
||||
| "community-context"
|
||||
| "community-structure"
|
||||
| "community-upload"
|
||||
|
||||
@@ -12,7 +12,8 @@ const PROPORTION_BY_STEP_INDEX: readonly ProportionBarState[] = [
|
||||
"1-2", // community-structure
|
||||
"1-3", // community-context
|
||||
"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-8", // review
|
||||
"2-0", // core-values (same fill as 1-8)
|
||||
|
||||
@@ -57,6 +57,12 @@ export const CREATE_FLOW_SCREEN_REGISTRY: Record<
|
||||
messageNamespace: "create.community.communitySize",
|
||||
centeredBodyBelowMd: false,
|
||||
},
|
||||
"setup-questions": {
|
||||
layoutKind: "select",
|
||||
figmaNodeId: "25224-105566",
|
||||
messageNamespace: "create.community.setupQuestions",
|
||||
centeredBodyBelowMd: false,
|
||||
},
|
||||
"community-context": {
|
||||
layoutKind: "text",
|
||||
figmaNodeId: "25224-106030",
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
* Step definitions and helpers for the Create Rule Flow
|
||||
*
|
||||
* 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).
|
||||
* `setup-questions` sits after size and before the logo upload.
|
||||
*/
|
||||
|
||||
import type { CreateFlowStep } from "../types";
|
||||
@@ -17,6 +18,7 @@ export const FLOW_STEP_ORDER: readonly CreateFlowStep[] = [
|
||||
"community-structure",
|
||||
"community-context",
|
||||
"community-size",
|
||||
"setup-questions",
|
||||
"community-upload",
|
||||
"community-save",
|
||||
"review",
|
||||
@@ -49,8 +51,8 @@ export type CreateFlowNavigationOptions = {
|
||||
skipCommunitySave?: boolean;
|
||||
/**
|
||||
* Template **Use without changes**: collect community name, description, and
|
||||
* optional photo only. Skips intro, structure, size, save, community review,
|
||||
* and the custom-rule authoring segment.
|
||||
* optional photo only. Skips intro, structure, size, setup questions, save,
|
||||
* community review, and the custom-rule authoring segment.
|
||||
*/
|
||||
useWithoutChangesIdentityOnly?: boolean;
|
||||
};
|
||||
@@ -65,6 +67,7 @@ const USE_WITHOUT_CHANGES_IDENTITY_SKIP = new Set<CreateFlowStep>([
|
||||
"informational",
|
||||
"community-structure",
|
||||
"community-size",
|
||||
"setup-questions",
|
||||
"community-save",
|
||||
"review",
|
||||
"core-values",
|
||||
@@ -199,6 +202,7 @@ export function shouldOfferCreateFlowSaveAndExit(
|
||||
*/
|
||||
export const CREATE_FLOW_SELECT_SPLIT_SCROLL_STEPS: readonly CreateFlowStep[] = [
|
||||
"community-size",
|
||||
"setup-questions",
|
||||
"community-structure",
|
||||
"core-values",
|
||||
"communication-methods",
|
||||
|
||||
@@ -35,9 +35,14 @@ export function CheckboxGroupView({
|
||||
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
|
||||
className={`text-small-paragraph ${
|
||||
className={`text-paragraph-m ${
|
||||
mode === "inverse"
|
||||
? "text-[var(--color-content-invert-primary)]"
|
||||
: "text-[var(--color-content-default-primary)]"
|
||||
|
||||
+15
-14
@@ -6,11 +6,11 @@ Product/engineering reference for the **custom** “Create rule” experience: U
|
||||
|
||||
## 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) |
|
||||
| --- | --- | --- |
|
||||
| **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` |
|
||||
| **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` |
|
||||
| 4 | Create Community | `community-context` | `/create/community-context` |
|
||||
| 5 | Create Community | `community-size` | `/create/community-size` |
|
||||
| 6 | Create Community | `community-upload` | `/create/community-upload` |
|
||||
| 7 | Create Community | `community-save` | `/create/community-save` |
|
||||
| 8 | Create Community (review frame) | `review` | `/create/review` |
|
||||
| 9 | Create Custom CommunityRule | `core-values` | `/create/core-values` |
|
||||
| 10 | Create Custom CommunityRule | `communication-methods` | `/create/communication-methods` |
|
||||
| 11 | Create Custom CommunityRule | `membership-methods` | `/create/membership-methods` |
|
||||
| 12 | Create Custom CommunityRule | `decision-approaches` | `/create/decision-approaches` |
|
||||
| 13 | Create Custom CommunityRule | `conflict-management` | `/create/conflict-management` |
|
||||
| 14 | Review and complete | `confirm-stakeholders` | `/create/confirm-stakeholders` |
|
||||
| 15 | Review and complete | `final-review` | `/create/final-review` |
|
||||
| 16 | Review and complete | `completed` | `/create/completed` |
|
||||
| 6 | Create Community | `setup-questions` | `/create/setup-questions` |
|
||||
| 7 | Create Community | `community-upload` | `/create/community-upload` |
|
||||
| 8 | Create Community | `community-save` | `/create/community-save` |
|
||||
| 9 | Create Community (review frame) | `review` | `/create/review` |
|
||||
| 10 | Create Custom CommunityRule | `core-values` | `/create/core-values` |
|
||||
| 11 | Create Custom CommunityRule | `communication-methods` | `/create/communication-methods` |
|
||||
| 12 | Create Custom CommunityRule | `membership-methods` | `/create/membership-methods` |
|
||||
| 13 | Create Custom CommunityRule | `decision-approaches` | `/create/decision-approaches` |
|
||||
| 14 | Create Custom CommunityRule | `conflict-management` | `/create/conflict-management` |
|
||||
| 15 | Review and complete | `confirm-stakeholders` | `/create/confirm-stakeholders` |
|
||||
| 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)).
|
||||
|
||||
@@ -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.
|
||||
|
||||
- **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.
|
||||
|
||||
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`.
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
{
|
||||
"next": "Next",
|
||||
"continue": "Continue",
|
||||
"skip": "Skip",
|
||||
"skipForNow": "Skip for now",
|
||||
"saveLater": "Skip for now",
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,7 @@ import createCommunityName from "./create/community/communityName.json";
|
||||
import createCommunityStructure from "./create/community/communityStructure.json";
|
||||
import createCommunityContext from "./create/community/communityContext.json";
|
||||
import createCommunitySize from "./create/community/communitySize.json";
|
||||
import createSetupQuestions from "./create/community/setupQuestions.json";
|
||||
import createCommunityUpload from "./create/community/communityUpload.json";
|
||||
import createCommunitySave from "./create/community/communitySave.json";
|
||||
import createReview from "./create/community/review.json";
|
||||
@@ -119,6 +120,7 @@ export default {
|
||||
communityStructure: createCommunityStructure,
|
||||
communityContext: createCommunityContext,
|
||||
communitySize: createCommunitySize,
|
||||
setupQuestions: createSetupQuestions,
|
||||
communityUpload: createCommunityUpload,
|
||||
communitySave: createCommunitySave,
|
||||
review: createReview,
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
@@ -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", () => {
|
||||
const r = buildPublishPayload({ title: "Oak Park Commons" });
|
||||
expect(r.ok).toBe(true);
|
||||
|
||||
@@ -58,6 +58,9 @@ describe("createFlowLayoutTokens", () => {
|
||||
expect(getCreateFlowContentMaxClass({ step: "community-structure" })).toBe(
|
||||
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
|
||||
);
|
||||
expect(getCreateFlowContentMaxClass({ step: "setup-questions" })).toBe(
|
||||
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
|
||||
);
|
||||
expect(
|
||||
getCreateFlowContentMaxClass({ step: "communication-methods" }),
|
||||
).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(
|
||||
"1-6",
|
||||
);
|
||||
|
||||
@@ -69,6 +69,28 @@ describe("createFlowStateSchema", () => {
|
||||
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", () => {
|
||||
const r = createFlowStateSchema.safeParse({ title: "x".repeat(600) });
|
||||
expect(r.success).toBe(false);
|
||||
|
||||
@@ -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", () => {
|
||||
expect(messages.create.footer.skip).toBe("Skip");
|
||||
expect(messages.create.footer.skipForNow).toBe("Skip for now");
|
||||
|
||||
@@ -69,6 +69,14 @@ describe("flowSteps", () => {
|
||||
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", () => {
|
||||
const opts = { skipCommunitySave: true } as const;
|
||||
expect(getNextStep("community-upload", opts)).toBe("review");
|
||||
@@ -77,7 +85,8 @@ describe("flowSteps", () => {
|
||||
|
||||
it("skipCommunitySave does not change steps outside the save segment", () => {
|
||||
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(getPreviousStep("communication-methods", opts)).toBe("core-values");
|
||||
});
|
||||
@@ -86,6 +95,8 @@ describe("flowSteps", () => {
|
||||
const opts = { useWithoutChangesIdentityOnly: true } as const;
|
||||
expect(getNextStep("community-name", opts)).toBe("community-context");
|
||||
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("confirm-stakeholders", opts)).toBe("final-review");
|
||||
expect(getPreviousStep("community-context", opts)).toBe("community-name");
|
||||
@@ -199,6 +210,7 @@ describe("flowSteps", () => {
|
||||
expect(shouldOfferCreateFlowSaveAndExit("informational")).toBe(false);
|
||||
expect(shouldOfferCreateFlowSaveAndExit("community-name")).toBe(false);
|
||||
expect(shouldOfferCreateFlowSaveAndExit("community-structure")).toBe(false);
|
||||
expect(shouldOfferCreateFlowSaveAndExit("setup-questions")).toBe(false);
|
||||
expect(shouldOfferCreateFlowSaveAndExit("community-upload")).toBe(false);
|
||||
expect(shouldOfferCreateFlowSaveAndExit("community-save")).toBe(true);
|
||||
expect(shouldOfferCreateFlowSaveAndExit("review")).toBe(true);
|
||||
@@ -244,6 +256,7 @@ describe("flowSteps", () => {
|
||||
expect(createFlowStepUsesSelectSplitScroll("community-structure")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(createFlowStepUsesSelectSplitScroll("setup-questions")).toBe(true);
|
||||
expect(createFlowStepUsesSelectSplitScroll("communication-methods")).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -19,6 +19,13 @@ describe("maturity and chip aliases", () => {
|
||||
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", () => {
|
||||
const fromId = migrateLegacyCreateFlowState({
|
||||
selectedMaturityIds: ["4"],
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user