Show the whole Rule on one review card, with quiet flags for missing cards and Continue once each question has one.
This commit is contained in:
1 parent
cc878e3c19
commit
6e2e1915ae
30 files changed
+571
-218
No files matched your search
@@ -64,4 +64,14 @@ describe("Chip", () => {
|
||||
expect(chip).toHaveAttribute("aria-pressed", "true");
|
||||
expect(chip.querySelector("svg[aria-hidden]")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("shows a warning icon and names the missing words", () => {
|
||||
render(
|
||||
<Chip label="Signal" state="unselected" hasWarning />,
|
||||
);
|
||||
const chip = screen.getByRole("button", {
|
||||
name: "Signal, missing words",
|
||||
});
|
||||
expect(chip.querySelector("svg")).not.toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -14,7 +14,9 @@ import { clearCoreValueDetailsLocalStorage } from "../../app/(app)/create/utils/
|
||||
import type { CreateFlowState } from "../../app/(app)/create/types";
|
||||
|
||||
function catalogChipName(label: string) {
|
||||
return new RegExp(`^(Select|Deselect) ${label}$`);
|
||||
return new RegExp(
|
||||
`^((Select|Deselect) ${label}|${label}, missing words)$`,
|
||||
);
|
||||
}
|
||||
|
||||
async function confirmDiscardCustomizeEdits() {
|
||||
@@ -75,7 +77,7 @@ describe("FinalReviewScreen", () => {
|
||||
render(<FinalReviewScreen />);
|
||||
expect(
|
||||
screen.getByRole("heading", {
|
||||
name: "Review your CommunityRule",
|
||||
name: "Review your Rule",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
@@ -83,9 +85,7 @@ describe("FinalReviewScreen", () => {
|
||||
it("renders lockup description", () => {
|
||||
render(<FinalReviewScreen />);
|
||||
expect(
|
||||
screen.getByText(
|
||||
/Here's what other people will see. Make sure everything looks good before you publish. Once the rule is published, you must use one of your decision-making mechanisms to edit it again./i,
|
||||
),
|
||||
screen.getByText("Here's what other people will see."),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -182,11 +182,19 @@ describe("FinalReviewScreen", () => {
|
||||
expect(screen.getByText("Conflict management")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders category chips", () => {
|
||||
it("shows add chips and flags when no cards or values are picked", () => {
|
||||
render(<FinalReviewScreen />);
|
||||
expect(screen.getByText("Consciousness")).toBeInTheDocument();
|
||||
expect(screen.getByText("Signal")).toBeInTheDocument();
|
||||
expect(screen.getByText("Open Admission")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("No values yet. You can add them any time."),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getAllByText("No card yet. Add one before you finish.").length,
|
||||
).toBe(4);
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Add value" }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getAllByRole("button", { name: "Add card" })).toHaveLength(4);
|
||||
expect(screen.queryByText("Consciousness")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -225,10 +233,10 @@ describe("FinalReviewScreen — prefilled selections", () => {
|
||||
expect(screen.getByText("Lazy consensus")).toBeInTheDocument();
|
||||
expect(screen.getByText("Conflict ladder")).toBeInTheDocument();
|
||||
|
||||
// Demo chips from `finalReview.json` must not leak through once the
|
||||
// user has real selections. The fallback still says "Open Admission".
|
||||
expect(screen.queryByText("Open Admission")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Consciousness")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Signal, missing words" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("warns when binding arbitration sits next to a join that records no yes", async () => {
|
||||
@@ -1293,15 +1301,15 @@ describe("FinalReviewScreen — edit published title and description", () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not expose click-to-edit title or description on default final review", () => {
|
||||
it("exposes click-to-edit title and description on default final review", () => {
|
||||
render(
|
||||
<FinalReviewWithFlowState
|
||||
title="Oak"
|
||||
communityContext="Visible body"
|
||||
/>,
|
||||
);
|
||||
expect(screen.queryByTestId("rule-title-edit")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("rule-description-edit")).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("rule-title-edit")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("rule-description-edit")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens Save modal from title click and updates title", async () => {
|
||||
|
||||
@@ -287,4 +287,18 @@ describe("MultiSelect – behaviour specifics", () => {
|
||||
);
|
||||
expect(screen.getByText("Pick one")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows a flag after the add chip in the same row", () => {
|
||||
render(
|
||||
<MultiSelect
|
||||
options={[]}
|
||||
addButton
|
||||
addButtonText="Add card"
|
||||
flag="No card yet. Add one before you finish."
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.getByText("No card yet. Add one before you finish."),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -10,9 +10,21 @@ const NAMES = {
|
||||
conflict: "Conflict management",
|
||||
};
|
||||
|
||||
function rowsWith(
|
||||
chipsByName: Partial<Record<string, string[]>>,
|
||||
): { name: string; chips: string[] }[] {
|
||||
return [
|
||||
"Values",
|
||||
"Communication",
|
||||
"Membership",
|
||||
"Decision-making",
|
||||
"Conflict management",
|
||||
].map((name) => ({ name, chips: chipsByName[name] ?? [] }));
|
||||
}
|
||||
|
||||
describe("buildFinalReviewCategoriesFromState", () => {
|
||||
it("returns [] when state has no selections and no sections", () => {
|
||||
expect(buildFinalReviewCategoriesFromState({}, NAMES)).toEqual([]);
|
||||
it("returns five empty rows when state has no selections and no sections", () => {
|
||||
expect(buildFinalReviewCategoriesFromState({}, NAMES)).toEqual(rowsWith({}));
|
||||
});
|
||||
|
||||
it("resolves method ids to labels from message presets", () => {
|
||||
@@ -33,7 +45,7 @@ describe("buildFinalReviewCategoriesFromState", () => {
|
||||
expect(byName.get("Membership")).toEqual(["Open admission"]);
|
||||
expect(byName.get("Decision-making")).toEqual(["Lazy consensus"]);
|
||||
expect(byName.get("Conflict management")).toEqual(["Conflict ladder"]);
|
||||
expect(byName.has("Values")).toBe(false);
|
||||
expect(byName.get("Values")).toEqual([]);
|
||||
});
|
||||
|
||||
it("derives core values via buildCoreValuesForDocument (snapshot + selected ids)", () => {
|
||||
@@ -46,9 +58,9 @@ describe("buildFinalReviewCategoriesFromState", () => {
|
||||
],
|
||||
};
|
||||
const rows = buildFinalReviewCategoriesFromState(state, NAMES);
|
||||
expect(rows).toEqual([
|
||||
{ name: "Values", chips: ["Accessibility", "Resilience"] },
|
||||
]);
|
||||
expect(rows).toEqual(
|
||||
rowsWith({ Values: ["Accessibility", "Resilience"] }),
|
||||
);
|
||||
});
|
||||
|
||||
it("drops unknown ids silently instead of inserting empty labels", () => {
|
||||
@@ -56,7 +68,7 @@ describe("buildFinalReviewCategoriesFromState", () => {
|
||||
selectedCommunicationMethodIds: ["signal", "bogus-id"],
|
||||
};
|
||||
const rows = buildFinalReviewCategoriesFromState(state, NAMES);
|
||||
expect(rows).toEqual([{ name: "Communication", chips: ["Signal"] }]);
|
||||
expect(rows).toEqual(rowsWith({ Communication: ["Signal"] }));
|
||||
});
|
||||
|
||||
it("resolves user-authored method ids from customMethodCardMetaById", () => {
|
||||
@@ -77,7 +89,7 @@ describe("buildFinalReviewCategoriesFromState", () => {
|
||||
selectedCommunicationMethodIds: ["signal", "signal"],
|
||||
};
|
||||
const rows = buildFinalReviewCategoriesFromState(state, NAMES);
|
||||
expect(rows).toEqual([{ name: "Communication", chips: ["Signal"] }]);
|
||||
expect(rows).toEqual(rowsWith({ Communication: ["Signal"] }));
|
||||
});
|
||||
|
||||
it("uses section titles for method facets when selections were cleared (use-without-changes)", () => {
|
||||
@@ -98,10 +110,12 @@ describe("buildFinalReviewCategoriesFromState", () => {
|
||||
selectedCommunicationMethodIds: [],
|
||||
};
|
||||
const rows = buildFinalReviewCategoriesFromState(state, NAMES);
|
||||
expect(rows).toEqual([
|
||||
{ name: "Values", chips: ["Consciousness", "Ecology"] },
|
||||
{ name: "Communication", chips: ["Signal"] },
|
||||
]);
|
||||
expect(rows).toEqual(
|
||||
rowsWith({
|
||||
Values: ["Consciousness", "Ecology"],
|
||||
Communication: ["Signal"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("when sections exist but facet selections are set, matches publish pickMethodIds (state wins)", () => {
|
||||
@@ -115,9 +129,9 @@ describe("buildFinalReviewCategoriesFromState", () => {
|
||||
selectedCommunicationMethodIds: ["in-person-meetings"],
|
||||
};
|
||||
const rows = buildFinalReviewCategoriesFromState(state, NAMES);
|
||||
expect(rows).toEqual([
|
||||
{ name: "Communication", chips: ["In-person meetings"] },
|
||||
]);
|
||||
expect(rows).toEqual(
|
||||
rowsWith({ Communication: ["In-person meetings"] }),
|
||||
);
|
||||
});
|
||||
|
||||
it("shows custom communication chips when sections exist and selections include a UUID", () => {
|
||||
@@ -153,10 +167,12 @@ describe("buildFinalReviewCategoriesFromState", () => {
|
||||
],
|
||||
};
|
||||
const rows = buildFinalReviewCategoriesFromState(state, NAMES);
|
||||
expect(rows).toEqual([
|
||||
{ name: "Values", chips: ["Accessibility"] },
|
||||
{ name: "Communication", chips: ["Signal"] },
|
||||
]);
|
||||
expect(rows).toEqual(
|
||||
rowsWith({
|
||||
Values: ["Accessibility"],
|
||||
Communication: ["Signal"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("prefers the chip snapshot over a duplicated Values section in sections", () => {
|
||||
@@ -177,8 +193,6 @@ describe("buildFinalReviewCategoriesFromState", () => {
|
||||
],
|
||||
};
|
||||
const rows = buildFinalReviewCategoriesFromState(state, NAMES);
|
||||
expect(rows).toEqual([
|
||||
{ name: "Values", chips: ["Accessibility"] },
|
||||
]);
|
||||
expect(rows).toEqual(rowsWith({ Values: ["Accessibility"] }));
|
||||
});
|
||||
});
|
||||
@@ -20,7 +20,11 @@ describe("create footer messages", () => {
|
||||
expect(messages.create.footer.confirmSize).toBe("Confirm size");
|
||||
});
|
||||
|
||||
it("exposes publishCommunityRule for the final-review footer CTA", () => {
|
||||
it("exposes Continue for the final-review footer CTA", () => {
|
||||
expect(messages.create.footer.continue).toBe("Continue");
|
||||
});
|
||||
|
||||
it("exposes publishCommunityRule for the edit-rule footer CTA", () => {
|
||||
expect(messages.create.footer.publishCommunityRule).toBe(
|
||||
"Publish CommunityRule",
|
||||
);
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { isFinalReviewContinueBlocked } from "../../app/(app)/create/utils/createFlowFooterLabels";
|
||||
import { communityInitialsFromTitle } from "../../lib/create/communityInitials";
|
||||
import { finalReviewChipHasWarning } from "../../lib/create/finalReviewChipWarnings";
|
||||
import type { CreateFlowState } from "../../app/(app)/create/types";
|
||||
|
||||
const completeCards: CreateFlowState = {
|
||||
title: "Oak Park Commons",
|
||||
selectedCommunicationMethodIds: ["signal"],
|
||||
selectedMembershipMethodIds: ["open-admission"],
|
||||
selectedDecisionApproachIds: ["lazy-consensus"],
|
||||
selectedConflictManagementIds: ["conflict-ladder"],
|
||||
};
|
||||
|
||||
describe("isFinalReviewContinueBlocked", () => {
|
||||
it("blocks when the community name is empty", () => {
|
||||
expect(
|
||||
isFinalReviewContinueBlocked({
|
||||
...completeCards,
|
||||
title: " ",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("blocks when any of the four questions has no card", () => {
|
||||
expect(
|
||||
isFinalReviewContinueBlocked({
|
||||
...completeCards,
|
||||
selectedMembershipMethodIds: [],
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not block when values are empty and the four questions have cards", () => {
|
||||
expect(isFinalReviewContinueBlocked(completeCards)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("finalReviewChipHasWarning", () => {
|
||||
it("warns when a value is missing meaning or signals", () => {
|
||||
expect(
|
||||
finalReviewChipHasWarning(
|
||||
{
|
||||
coreValueDetailsByChipId: {
|
||||
"1": { meaning: "Care", signals: "" },
|
||||
},
|
||||
},
|
||||
"coreValues",
|
||||
"1",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("warns when a method card has no saved words", () => {
|
||||
expect(
|
||||
finalReviewChipHasWarning({}, "communication", "signal"),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not warn when meaning and signals are both filled", () => {
|
||||
expect(
|
||||
finalReviewChipHasWarning(
|
||||
{
|
||||
coreValueDetailsByChipId: {
|
||||
"1": { meaning: "Care", signals: "We talk over people." },
|
||||
},
|
||||
},
|
||||
"coreValues",
|
||||
"1",
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("communityInitialsFromTitle", () => {
|
||||
it("uses the first letters of the first two words", () => {
|
||||
expect(communityInitialsFromTitle("Oak Park Commons")).toBe("OP");
|
||||
});
|
||||
|
||||
it("uses two letters from a single word", () => {
|
||||
expect(communityInitialsFromTitle("Neighbors")).toBe("NE");
|
||||
});
|
||||
});
|
||||
@@ -16,9 +16,9 @@ import {
|
||||
} from "../../app/(app)/create/utils/flowSteps";
|
||||
|
||||
describe("flowSteps", () => {
|
||||
it("places confirm-stakeholders immediately before final-review", () => {
|
||||
const i = FLOW_STEP_ORDER.indexOf("confirm-stakeholders");
|
||||
const j = FLOW_STEP_ORDER.indexOf("final-review");
|
||||
it("places final-review immediately before confirm-stakeholders", () => {
|
||||
const i = FLOW_STEP_ORDER.indexOf("final-review");
|
||||
const j = FLOW_STEP_ORDER.indexOf("confirm-stakeholders");
|
||||
expect(i).toBeGreaterThanOrEqual(0);
|
||||
expect(j).toBe(i + 1);
|
||||
});
|
||||
@@ -27,8 +27,8 @@ describe("flowSteps", () => {
|
||||
expect(getNextStep("communication-methods")).toBe("membership-methods");
|
||||
expect(getNextStep("membership-methods")).toBe("decision-approaches");
|
||||
expect(getNextStep("decision-approaches")).toBe("conflict-management");
|
||||
expect(getNextStep("conflict-management")).toBe("confirm-stakeholders");
|
||||
expect(getNextStep("confirm-stakeholders")).toBe("final-review");
|
||||
expect(getNextStep("conflict-management")).toBe("final-review");
|
||||
expect(getNextStep("final-review")).toBe("confirm-stakeholders");
|
||||
});
|
||||
|
||||
it("getNextStep returns null for last step or invalid", () => {
|
||||
@@ -39,7 +39,8 @@ describe("flowSteps", () => {
|
||||
});
|
||||
|
||||
it("getPreviousStep returns prior step or null", () => {
|
||||
expect(getPreviousStep("final-review")).toBe("confirm-stakeholders");
|
||||
expect(getPreviousStep("final-review")).toBe("conflict-management");
|
||||
expect(getPreviousStep("confirm-stakeholders")).toBe("final-review");
|
||||
expect(getPreviousStep("informational")).toBeNull();
|
||||
expect(getPreviousStep(null)).toBeNull();
|
||||
});
|
||||
@@ -91,19 +92,17 @@ describe("flowSteps", () => {
|
||||
expect(getPreviousStep("communication-methods", opts)).toBe("core-values");
|
||||
});
|
||||
|
||||
it("useWithoutChangesIdentityOnly walks name → description → photo → stakeholders", () => {
|
||||
it("useWithoutChangesIdentityOnly walks name → description → photo → final-review", () => {
|
||||
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(getNextStep("community-upload", opts)).toBe("final-review");
|
||||
expect(getNextStep("final-review", opts)).toBe("confirm-stakeholders");
|
||||
expect(getPreviousStep("community-context", opts)).toBe("community-name");
|
||||
expect(getPreviousStep("community-upload", opts)).toBe("community-context");
|
||||
expect(getPreviousStep("confirm-stakeholders", opts)).toBe(
|
||||
"community-upload",
|
||||
);
|
||||
expect(getPreviousStep("final-review", opts)).toBe("community-upload");
|
||||
expect(getPreviousStep("community-name", opts)).toBeNull();
|
||||
});
|
||||
|
||||
@@ -112,16 +111,14 @@ describe("flowSteps", () => {
|
||||
skipCommunitySave: true,
|
||||
useWithoutChangesIdentityOnly: true,
|
||||
} as const;
|
||||
expect(getNextStep("community-upload", opts)).toBe("confirm-stakeholders");
|
||||
expect(getPreviousStep("confirm-stakeholders", opts)).toBe(
|
||||
"community-upload",
|
||||
);
|
||||
expect(getNextStep("community-upload", opts)).toBe("final-review");
|
||||
expect(getPreviousStep("final-review", opts)).toBe("community-upload");
|
||||
});
|
||||
|
||||
it("resolveCreateFlowBackTarget returns template review when use-without slug is set on confirm-stakeholders", () => {
|
||||
it("resolveCreateFlowBackTarget returns template review when use-without slug is set on final-review", () => {
|
||||
expect(
|
||||
resolveCreateFlowBackTarget(
|
||||
"confirm-stakeholders",
|
||||
"final-review",
|
||||
undefined,
|
||||
"mutual-aid-mondays",
|
||||
),
|
||||
@@ -138,10 +135,10 @@ describe("flowSteps", () => {
|
||||
).toEqual({ kind: "templateReview", slug: "mutual-aid-mondays" });
|
||||
});
|
||||
|
||||
it("resolveCreateFlowBackTarget uses photo step behind stakeholders on the identity-only path", () => {
|
||||
it("resolveCreateFlowBackTarget uses photo step behind final-review on the identity-only path", () => {
|
||||
expect(
|
||||
resolveCreateFlowBackTarget(
|
||||
"confirm-stakeholders",
|
||||
"final-review",
|
||||
{ useWithoutChangesIdentityOnly: true },
|
||||
"mutual-aid-mondays",
|
||||
),
|
||||
@@ -151,13 +148,13 @@ describe("flowSteps", () => {
|
||||
it("resolveCreateFlowBackTarget falls back to linear previous when slug is absent", () => {
|
||||
expect(
|
||||
resolveCreateFlowBackTarget("confirm-stakeholders", undefined, undefined),
|
||||
).toEqual({ kind: "step", step: "conflict-management" });
|
||||
).toEqual({ kind: "step", step: "final-review" });
|
||||
});
|
||||
|
||||
it("resolveCreateFlowBackTarget ignores whitespace-only slug", () => {
|
||||
expect(
|
||||
resolveCreateFlowBackTarget("confirm-stakeholders", undefined, " "),
|
||||
).toEqual({ kind: "step", step: "conflict-management" });
|
||||
).toEqual({ kind: "step", step: "final-review" });
|
||||
});
|
||||
|
||||
it("buildTemplateReviewHref encodes slug and optional fromFlow query", () => {
|
||||
|
||||
@@ -129,7 +129,7 @@ describe("useTemplateReviewActions", () => {
|
||||
await result.current.handleUseWithoutChanges();
|
||||
});
|
||||
|
||||
expect(router.push).toHaveBeenCalledWith("/create/confirm-stakeholders");
|
||||
expect(router.push).toHaveBeenCalledWith("/create/final-review");
|
||||
expect(applied?.title).toBe("Neighborhood");
|
||||
expect(applied?.communityContext).toBe("We meet weekly");
|
||||
expect(applied?.setupTraits).toEqual({ employer: true });
|
||||
|
||||
Reference in new issue
Block a user