Create custom flow UI

This commit is contained in:
adilallo
2026-04-17 22:25:24 -06:00
parent eedb70f9f3
commit 36dcb79870
38 changed files with 1227 additions and 250 deletions
+4 -3
View File
@@ -40,8 +40,8 @@ describe("ProportionBar (behavioral tests)", () => {
it("renders proportion bar with correct progress value", () => {
render(<ProportionBar progress="2-1" />);
const progressbar = screen.getByRole("progressbar");
// 2-1: First section full (1) + second section 1/3 filled = 1 + 1/3 ≈ 1.333
expect(progressbar).toHaveAttribute("aria-valuenow", "1.3333333333333333");
// 2-1 (Figma `17861:33241`): first section full + second section 1/4 filled = 1.25.
expect(progressbar).toHaveAttribute("aria-valuenow", "1.25");
expect(progressbar).toHaveAttribute("aria-valuemin", "0");
expect(progressbar).toHaveAttribute("aria-valuemax", "3");
});
@@ -63,7 +63,8 @@ describe("ProportionBar (behavioral tests)", () => {
{ progress: "1-0" as const, expected: 1 / 6 }, // First section 1/6 filled
{ progress: "1-5" as const, expected: 1 }, // First section 6/6 filled (fully filled)
{ progress: "2-0" as const, expected: 1 }, // First section full, second empty
{ progress: "2-2" as const, expected: 1 + 2 / 3 }, // First section full, second section 2/3 filled
{ progress: "2-2" as const, expected: 1 + 1 / 2 }, // 1 + 1/2 per Figma `18861:15250`
{ progress: "2-3" as const, expected: 1 + 3 / 4 }, // 1 + 3/4 per Figma `21434:17632`
{ progress: "3-0" as const, expected: 2 }, // First two sections full, third empty
{ progress: "3-2" as const, expected: 2 + 2 / 3 }, // First two sections full, third section 2/3 filled
];
@@ -6,16 +6,16 @@ import {
} from "../utils/test-utils";
import userEvent from "@testing-library/user-event";
import { describe, test, expect, afterEach } from "vitest";
import { CardsScreen } from "../../app/create/screens/card/CardsScreen";
import { CommunicationMethodsScreen } from "../../app/create/screens/card/CommunicationMethodsScreen";
afterEach(() => {
cleanup();
});
describe("Create flow cards page", () => {
describe("Create flow communication-methods page", () => {
test("clicking a card opens the Create modal", async () => {
const user = userEvent.setup();
render(<CardsScreen />);
render(<CommunicationMethodsScreen />);
const signalCards = screen.getAllByRole("button", {
name: /Signal: Encrypted messaging/,
@@ -29,7 +29,7 @@ describe("Create flow cards page", () => {
});
test("renders without error", () => {
render(<CardsScreen />);
render(<CommunicationMethodsScreen />);
expect(
screen.getByText(
@@ -39,13 +39,12 @@ describe("Create flow cards page", () => {
});
test("renders HeaderLockup and CardStack content", () => {
render(<CardsScreen />);
render(<CommunicationMethodsScreen />);
expect(
screen.getByText(
"You can select multiple methods for different needs or add your own",
),
screen.getByText(/You can select multiple methods for different needs or/),
).toBeInTheDocument();
expect(screen.getByRole("button", { name: "add" })).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "See all communication approaches" }),
).toBeInTheDocument();
@@ -53,7 +52,7 @@ describe("Create flow cards page", () => {
test("toggle expands and shows Show less", async () => {
const user = userEvent.setup();
render(<CardsScreen />);
render(<CommunicationMethodsScreen />);
const toggle = screen.getByRole("button", {
name: "See all communication approaches",
+9 -9
View File
@@ -18,7 +18,7 @@ describe("Create flow right-rail page", () => {
expect(
screen.getByRole("heading", {
name: "How should conflicts be resolved?",
name: "How should this community make difficult decisions?",
}),
).toBeInTheDocument();
});
@@ -30,9 +30,9 @@ describe("Create flow right-rail page", () => {
if (element?.tagName !== "P") return false;
const text = element.textContent ?? "";
return (
text.includes("You can also combine or") &&
text.includes("Select as many as you need") &&
text.includes("add") &&
text.includes("new approaches to the list")
text.includes("new decision making approaches")
);
});
expect(description).toBeInTheDocument();
@@ -77,17 +77,17 @@ describe("Create flow right-rail page", () => {
expect(
screen.getByRole("button", {
name: /Mediation: Collaborative work to reach a resolution/,
name: /Lazy Consensus: A decision is assumed approved/,
}),
).toBeInTheDocument();
expect(
screen.getByRole("button", {
name: /Facilitated dialogue: Structured sessions/,
name: /Do-ocracy: Decisions are made by those who take initiative/,
}),
).toBeInTheDocument();
expect(
screen.getByRole("button", {
name: /Invite-only: Private discussions with selected participants/,
name: /Consensus Decision-Making: All members must agree/,
}),
).toBeInTheDocument();
});
@@ -123,10 +123,10 @@ describe("Create flow right-rail page", () => {
const user = userEvent.setup();
render(<RightRailScreen />);
const mediationCard = screen.getByRole("button", {
name: /Mediation: Collaborative work to reach a resolution/,
const card = screen.getByRole("button", {
name: /Lazy Consensus: A decision is assumed approved/,
});
await user.click(mediationCard);
await user.click(card);
expect(screen.getByText("SELECTED")).toBeInTheDocument();
});
@@ -26,4 +26,28 @@ describe("getProportionBarProgressForCreateFlowStep", () => {
"2-0",
);
});
it("uses 2-1 on communication-methods", () => {
expect(
getProportionBarProgressForCreateFlowStep("communication-methods"),
).toBe("2-1");
});
it("uses 2-2 on membership-methods", () => {
expect(
getProportionBarProgressForCreateFlowStep("membership-methods"),
).toBe("2-2");
});
it("uses 2-3 on decision-approaches (Figma Flow — Right Rail)", () => {
expect(
getProportionBarProgressForCreateFlowStep("decision-approaches"),
).toBe("2-3");
});
it("uses 3-0 on conflict-management (start of Review segment)", () => {
expect(
getProportionBarProgressForCreateFlowStep("conflict-management"),
).toBe("3-0");
});
});
+1 -1
View File
@@ -56,7 +56,7 @@ describe("createFlowStateSchema", () => {
it("accepts known fields and passthrough keys", () => {
const r = createFlowStateSchema.safeParse({
title: "My rule",
currentStep: "cards",
currentStep: "communication-methods",
customField: { nested: [1, 2] },
});
expect(r.success).toBe(true);
+5 -2
View File
@@ -16,7 +16,10 @@ describe("flowSteps", () => {
});
it("getNextStep returns next step in order", () => {
expect(getNextStep("right-rail")).toBe("confirm-stakeholders");
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");
});
@@ -67,6 +70,6 @@ describe("flowSteps", () => {
const opts = { skipCommunitySave: true } as const;
expect(getNextStep("community-size", opts)).toBe("community-upload");
expect(getNextStep("review", opts)).toBe("core-values");
expect(getPreviousStep("cards", opts)).toBe("core-values");
expect(getPreviousStep("communication-methods", opts)).toBe("core-values");
});
});
+12 -18
View File
@@ -2,27 +2,12 @@ import { describe, it, expect } from "vitest";
import { migrateLegacyCreateFlowState } from "../../lib/create/migrateLegacyCreateFlowState";
describe("migrateLegacyCreateFlowState", () => {
it("maps communityReflection to communitySaveEmail when save email empty", () => {
it("passes through object payloads", () => {
const out = migrateLegacyCreateFlowState({
title: "T",
communityReflection: "old@example.com",
});
expect(out.communitySaveEmail).toBe("old@example.com");
expect("communityReflection" in out).toBe(false);
});
it("does not overwrite existing communitySaveEmail", () => {
const out = migrateLegacyCreateFlowState({
communityReflection: "old@example.com",
communitySaveEmail: "kept@example.com",
});
expect(out.communitySaveEmail).toBe("kept@example.com");
});
it("rewrites currentStep slug", () => {
const out = migrateLegacyCreateFlowState({
currentStep: "community-reflection",
currentStep: "community-save",
});
expect(out.title).toBe("T");
expect(out.currentStep).toBe("community-save");
});
@@ -30,4 +15,13 @@ describe("migrateLegacyCreateFlowState", () => {
expect(migrateLegacyCreateFlowState(null)).toEqual({});
expect(migrateLegacyCreateFlowState(undefined)).toEqual({});
});
it("renames legacy right-rail step to decision-approaches", () => {
const out = migrateLegacyCreateFlowState({
currentStep: "right-rail",
title: "T",
});
expect(out.currentStep).toBe("decision-approaches");
expect(out.title).toBe("T");
});
});