Template flow cleaned up
This commit is contained in:
@@ -100,3 +100,46 @@ describe("FinalReviewScreen", () => {
|
||||
expect(screen.getByText("Open Admission")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Seeds a Customize-from-template style state (method ids + core-value
|
||||
* snapshot) and asserts the final-review RuleCard renders the resolved
|
||||
* labels — the fix for "preselected chips don't register on final review".
|
||||
*/
|
||||
function FinalReviewWithCustomizeSelections() {
|
||||
const { replaceState } = useCreateFlow();
|
||||
useLayoutEffect(() => {
|
||||
replaceState({
|
||||
title: "Oak Park Commons",
|
||||
selectedCoreValueIds: ["1"],
|
||||
coreValuesChipsSnapshot: [
|
||||
{ id: "1", label: "Accessibility", state: "selected" },
|
||||
{ id: "2", label: "Accountability", state: "unselected" },
|
||||
],
|
||||
selectedCommunicationMethodIds: ["signal"],
|
||||
selectedMembershipMethodIds: ["open-access"],
|
||||
selectedDecisionApproachIds: ["lazy-consensus"],
|
||||
selectedConflictManagementIds: ["peer-mediation"],
|
||||
});
|
||||
}, [replaceState]);
|
||||
return <FinalReviewScreen />;
|
||||
}
|
||||
|
||||
describe("FinalReviewScreen — prefilled selections", () => {
|
||||
it("renders chips resolved from selection ids, not demo fallbacks", async () => {
|
||||
render(<FinalReviewWithCustomizeSelections />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Accessibility")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText("Signal")).toBeInTheDocument();
|
||||
expect(screen.getByText("Open Access")).toBeInTheDocument();
|
||||
expect(screen.getByText("Lazy Consensus")).toBeInTheDocument();
|
||||
expect(screen.getByText("Peer Mediation")).toBeInTheDocument();
|
||||
|
||||
// Demo chips from `finalReview.json` must not leak through once the
|
||||
// user has real selections: "Open Admission" is shipped as fallback,
|
||||
// while the customize flow resolves to "Open Access".
|
||||
expect(screen.queryByText("Open Admission")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Consciousness")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { renderWithProviders as render, screen } from "../utils/test-utils";
|
||||
import { beforeEach, describe, it, expect } from "vitest";
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
renderWithProviders as render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from "../utils/test-utils";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { CommunityReviewScreen } from "../../app/(app)/create/screens/review/CommunityReviewScreen";
|
||||
import { useCreateFlow } from "../../app/(app)/create/context/CreateFlowContext";
|
||||
import { testRouter } from "../mocks/navigation";
|
||||
|
||||
describe("CommunityReviewScreen", () => {
|
||||
beforeEach(() => {
|
||||
testRouter.replace.mockReset();
|
||||
testRouter.push.mockReset();
|
||||
});
|
||||
|
||||
it("renders without crashing", () => {
|
||||
render(<CommunityReviewScreen />);
|
||||
expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument();
|
||||
@@ -27,18 +39,18 @@ describe("CommunityReviewScreen", () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders RuleCard with title", () => {
|
||||
it("renders RuleCard with title fallback when no community name is set", () => {
|
||||
render(<CommunityReviewScreen />);
|
||||
expect(screen.getByText("Mutual Aid Mondays")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders RuleCard with description", () => {
|
||||
it("omits the RuleCard description when the user has not entered community context", () => {
|
||||
render(<CommunityReviewScreen />);
|
||||
expect(
|
||||
screen.getByText(
|
||||
/Mutual Aid Monday is a grassroots community in Denver, founded in November 2020 by Kelsang Virya, dedicated to supporting neighbors experiencing homelessness./i,
|
||||
screen.queryByText(
|
||||
/Mutual Aid Monday is a grassroots community in Denver/i,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders RuleCard as a button (card is interactive)", () => {
|
||||
@@ -50,3 +62,60 @@ describe("CommunityReviewScreen", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Seeds `pendingTemplateAction` into CreateFlowContext before the screen
|
||||
* under test mounts, so we can assert its mount-time redirect behavior.
|
||||
* Mirrors the flow `handleCustomizeTemplate` / `handleUseTemplateWithoutChanges`
|
||||
* create when the user picks a template before completing community stage.
|
||||
*/
|
||||
function ReviewWithPendingAction({
|
||||
mode,
|
||||
}: {
|
||||
mode: "customize" | "useWithoutChanges";
|
||||
}) {
|
||||
const { state, updateState } = useCreateFlow();
|
||||
const seededRef = React.useRef(false);
|
||||
useEffect(() => {
|
||||
if (seededRef.current) return;
|
||||
seededRef.current = true;
|
||||
updateState({
|
||||
title: "Neighborhood",
|
||||
pendingTemplateAction: { slug: "mutual-aid-mondays", mode },
|
||||
});
|
||||
}, [mode, updateState]);
|
||||
// Block the real screen from mounting until the seed landed — otherwise
|
||||
// its own `useEffect` reads an empty state on the first pass and bails.
|
||||
if (!state.pendingTemplateAction) return null;
|
||||
return <CommunityReviewScreen />;
|
||||
}
|
||||
|
||||
describe("CommunityReviewScreen — pendingTemplateAction redirect", () => {
|
||||
beforeEach(() => {
|
||||
testRouter.replace.mockReset();
|
||||
testRouter.push.mockReset();
|
||||
});
|
||||
|
||||
it("redirects to /create/core-values when mode === 'customize'", async () => {
|
||||
render(<ReviewWithPendingAction mode="customize" />);
|
||||
await waitFor(() => {
|
||||
expect(testRouter.replace).toHaveBeenCalledWith("/create/core-values");
|
||||
});
|
||||
expect(testRouter.push).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("redirects to /create/confirm-stakeholders when mode === 'useWithoutChanges'", async () => {
|
||||
render(<ReviewWithPendingAction mode="useWithoutChanges" />);
|
||||
await waitFor(() => {
|
||||
expect(testRouter.replace).toHaveBeenCalledWith(
|
||||
"/create/confirm-stakeholders",
|
||||
);
|
||||
});
|
||||
expect(testRouter.push).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not redirect when no pendingTemplateAction is set", () => {
|
||||
render(<CommunityReviewScreen />);
|
||||
expect(testRouter.replace).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import React from "react";
|
||||
import { vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import TopNav from "../../app/components/navigation/TopNav";
|
||||
import { renderWithProviders } from "../utils/test-utils";
|
||||
import { CREATE_FLOW_ANONYMOUS_KEY } from "../../app/(app)/create/utils/anonymousDraftStorage";
|
||||
import { CORE_VALUE_DETAILS_STORAGE_KEY } from "../../app/(app)/create/utils/coreValueDetailsLocalStorage";
|
||||
import { componentTestSuite } from "../utils/componentTestSuite";
|
||||
|
||||
// Mock next/navigation (TopNav uses useRouter for Create Rule button and usePathname for nav state)
|
||||
const { pushMock } = vi.hoisted(() => ({ pushMock: vi.fn() }));
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({
|
||||
push: vi.fn(),
|
||||
push: pushMock,
|
||||
replace: vi.fn(),
|
||||
prefetch: vi.fn(),
|
||||
back: vi.fn(),
|
||||
@@ -50,3 +57,45 @@ componentTestSuite<TopNavProps>({
|
||||
errorState: false,
|
||||
},
|
||||
});
|
||||
|
||||
describe('TopNav "Create rule" button', () => {
|
||||
beforeEach(() => {
|
||||
pushMock.mockReset();
|
||||
window.localStorage.clear();
|
||||
});
|
||||
afterEach(() => {
|
||||
window.localStorage.clear();
|
||||
});
|
||||
|
||||
/**
|
||||
* Guards against localStorage stickiness on the marketing homepage: hitting
|
||||
* the top-nav "Create rule" from anywhere outside `/create` must wipe the
|
||||
* in-flight anonymous draft so the wizard always starts fresh. See
|
||||
* handleCreateRuleClick in TopNav.container.tsx for the contract.
|
||||
*/
|
||||
it("clears anonymous draft + core-value-details localStorage before routing to /create", async () => {
|
||||
window.localStorage.setItem(
|
||||
CREATE_FLOW_ANONYMOUS_KEY,
|
||||
JSON.stringify({ title: "Stale community" }),
|
||||
);
|
||||
window.localStorage.setItem(
|
||||
CORE_VALUE_DETAILS_STORAGE_KEY,
|
||||
JSON.stringify({ "1": { meaning: "m", signals: "s" } }),
|
||||
);
|
||||
|
||||
renderWithProviders(<TopNav folderTop={false} />);
|
||||
|
||||
// TopNav renders the Create Rule button at three breakpoints (xs/sm/md);
|
||||
// any of them clicking the same handler is the point.
|
||||
const [btn] = screen.getAllByRole("button", {
|
||||
name: /create a new rule/i,
|
||||
});
|
||||
await userEvent.click(btn);
|
||||
|
||||
expect(window.localStorage.getItem(CREATE_FLOW_ANONYMOUS_KEY)).toBeNull();
|
||||
expect(
|
||||
window.localStorage.getItem(CORE_VALUE_DETAILS_STORAGE_KEY),
|
||||
).toBeNull();
|
||||
expect(pushMock).toHaveBeenCalledWith("/create");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { act, render, screen } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import {
|
||||
CreateFlowProvider,
|
||||
useCreateFlow,
|
||||
} from "../../app/(app)/create/context/CreateFlowContext";
|
||||
|
||||
/**
|
||||
* Harness: mounts a consumer that renders the state we want to assert on and
|
||||
* exposes imperative handles (updateState, resetCustomRuleSelections) via
|
||||
* window globals. Keeps the test readable vs. threading refs everywhere.
|
||||
*/
|
||||
function Harness() {
|
||||
const { state, updateState, resetCustomRuleSelections } = useCreateFlow();
|
||||
(window as unknown as { __updateState: typeof updateState }).__updateState =
|
||||
updateState;
|
||||
(
|
||||
window as unknown as { __resetCustomRule: typeof resetCustomRuleSelections }
|
||||
).__resetCustomRule = resetCustomRuleSelections;
|
||||
return (
|
||||
<>
|
||||
<div data-testid="title">{state.title ?? ""}</div>
|
||||
<div data-testid="core">
|
||||
{(state.selectedCoreValueIds ?? []).join(",")}
|
||||
</div>
|
||||
<div data-testid="comm">
|
||||
{(state.selectedCommunicationMethodIds ?? []).join(",")}
|
||||
</div>
|
||||
<div data-testid="details">
|
||||
{Object.keys(state.coreValueDetailsByChipId ?? {}).join(",")}
|
||||
</div>
|
||||
<div data-testid="snapshot">
|
||||
{(state.coreValuesChipsSnapshot ?? []).map((r) => r.id).join(",")}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function getUpdateState() {
|
||||
return (window as unknown as { __updateState: (u: unknown) => void })
|
||||
.__updateState;
|
||||
}
|
||||
|
||||
function getResetCustomRule() {
|
||||
return (window as unknown as { __resetCustomRule: () => void })
|
||||
.__resetCustomRule;
|
||||
}
|
||||
|
||||
describe("CreateFlowContext — resetCustomRuleSelections", () => {
|
||||
it("clears all custom-rule stage selections while keeping community stage", () => {
|
||||
render(
|
||||
<CreateFlowProvider>
|
||||
<Harness />
|
||||
</CreateFlowProvider>,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
getUpdateState()({
|
||||
title: "Mutual Aid Mondays",
|
||||
communityContext: "Neighborhood",
|
||||
selectedCoreValueIds: ["1", "3"],
|
||||
coreValuesChipsSnapshot: [
|
||||
{ id: "1", label: "Trust", state: "selected" },
|
||||
],
|
||||
coreValueDetailsByChipId: {
|
||||
"1": { meaning: "m", signals: "s" },
|
||||
},
|
||||
selectedCommunicationMethodIds: ["consensus-decision-making"],
|
||||
selectedMembershipMethodIds: ["open"],
|
||||
selectedDecisionApproachIds: ["consensus-decision-making"],
|
||||
selectedConflictManagementIds: ["mediation"],
|
||||
});
|
||||
});
|
||||
|
||||
expect(screen.getByTestId("title").textContent).toBe("Mutual Aid Mondays");
|
||||
expect(screen.getByTestId("core").textContent).toBe("1,3");
|
||||
expect(screen.getByTestId("comm").textContent).toBe(
|
||||
"consensus-decision-making",
|
||||
);
|
||||
|
||||
act(() => {
|
||||
getResetCustomRule()();
|
||||
});
|
||||
|
||||
expect(screen.getByTestId("title").textContent).toBe("Mutual Aid Mondays");
|
||||
expect(screen.getByTestId("core").textContent).toBe("");
|
||||
expect(screen.getByTestId("comm").textContent).toBe("");
|
||||
expect(screen.getByTestId("details").textContent).toBe("");
|
||||
expect(screen.getByTestId("snapshot").textContent).toBe("");
|
||||
});
|
||||
|
||||
it("is a no-op when no custom-rule selections were set", () => {
|
||||
render(
|
||||
<CreateFlowProvider>
|
||||
<Harness />
|
||||
</CreateFlowProvider>,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
getUpdateState()({ title: "Just a Community" });
|
||||
});
|
||||
|
||||
act(() => {
|
||||
getResetCustomRule()();
|
||||
});
|
||||
|
||||
expect(screen.getByTestId("title").textContent).toBe("Just a Community");
|
||||
expect(screen.getByTestId("core").textContent).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -4,17 +4,35 @@ import {
|
||||
cleanup,
|
||||
} from "../utils/test-utils";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, test, expect, afterEach, beforeEach } from "vitest";
|
||||
import { describe, test, expect, afterEach, beforeEach, vi } from "vitest";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import TemplatesPageClient from "../../app/(marketing)/templates/TemplatesPageClient";
|
||||
import { testRouter } from "../mocks/navigation";
|
||||
import { GOVERNANCE_TEMPLATE_CATALOG } from "../../lib/templates/governanceTemplateCatalog";
|
||||
import { CREATE_FLOW_ANONYMOUS_KEY } from "../../app/(app)/create/utils/anonymousDraftStorage";
|
||||
import { CORE_VALUE_DETAILS_STORAGE_KEY } from "../../app/(app)/create/utils/coreValueDetailsLocalStorage";
|
||||
|
||||
/** Seed localStorage as if a stale anonymous draft were already in place. */
|
||||
function seedStaleDraft() {
|
||||
window.localStorage.setItem(
|
||||
CREATE_FLOW_ANONYMOUS_KEY,
|
||||
JSON.stringify({ title: "Stale Community" }),
|
||||
);
|
||||
window.localStorage.setItem(
|
||||
CORE_VALUE_DETAILS_STORAGE_KEY,
|
||||
JSON.stringify({ "1": { meaning: "stale", signals: "stale" } }),
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
testRouter.push.mockClear();
|
||||
vi.mocked(useSearchParams).mockReturnValue(new URLSearchParams());
|
||||
window.localStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
window.localStorage.clear();
|
||||
});
|
||||
|
||||
describe("Templates page (/templates)", () => {
|
||||
@@ -54,4 +72,51 @@ describe("Templates page (/templates)", () => {
|
||||
"/create/review-template/solidarity-network",
|
||||
);
|
||||
});
|
||||
|
||||
test("direct entry (no ?fromFlow=1): wipes anonymous draft before navigating", async () => {
|
||||
seedStaleDraft();
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<TemplatesPageClient initialGridEntries={GOVERNANCE_TEMPLATE_CATALOG} />,
|
||||
);
|
||||
|
||||
const consensusCard = screen.getByText("Consensus").closest("div");
|
||||
await user.click(consensusCard);
|
||||
|
||||
expect(window.localStorage.getItem(CREATE_FLOW_ANONYMOUS_KEY)).toBeNull();
|
||||
expect(
|
||||
window.localStorage.getItem(CORE_VALUE_DETAILS_STORAGE_KEY),
|
||||
).toBeNull();
|
||||
expect(testRouter.push).toHaveBeenCalledWith(
|
||||
"/create/review-template/consensus",
|
||||
);
|
||||
});
|
||||
|
||||
test("in-flow entry (?fromFlow=1): preserves the anonymous draft", async () => {
|
||||
vi.mocked(useSearchParams).mockReturnValue(
|
||||
new URLSearchParams("fromFlow=1"),
|
||||
);
|
||||
seedStaleDraft();
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<TemplatesPageClient initialGridEntries={GOVERNANCE_TEMPLATE_CATALOG} />,
|
||||
);
|
||||
|
||||
const consensusCard = screen.getByText("Consensus").closest("div");
|
||||
await user.click(consensusCard);
|
||||
|
||||
expect(window.localStorage.getItem(CREATE_FLOW_ANONYMOUS_KEY)).toBe(
|
||||
JSON.stringify({ title: "Stale Community" }),
|
||||
);
|
||||
expect(
|
||||
window.localStorage.getItem(CORE_VALUE_DETAILS_STORAGE_KEY),
|
||||
).toBe(
|
||||
JSON.stringify({ "1": { meaning: "stale", signals: "stale" } }),
|
||||
);
|
||||
// No `?fromFlow=1` on the outbound review-template URL — the marker
|
||||
// only disambiguates /templates' own click behavior.
|
||||
expect(testRouter.push).toHaveBeenCalledWith(
|
||||
"/create/review-template/consensus",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
GOVERNANCE_TEMPLATE_HOME_SLUGS,
|
||||
getGovernanceTemplatesForHome,
|
||||
} from "../../lib/templates/governanceTemplateCatalog";
|
||||
import { CREATE_FLOW_ANONYMOUS_KEY } from "../../app/(app)/create/utils/anonymousDraftStorage";
|
||||
import { CORE_VALUE_DETAILS_STORAGE_KEY } from "../../app/(app)/create/utils/coreValueDetailsLocalStorage";
|
||||
|
||||
const homeFeatured = getGovernanceTemplatesForHome();
|
||||
|
||||
@@ -212,6 +214,31 @@ describe("RuleStack Component", () => {
|
||||
debugSpy.mockRestore();
|
||||
});
|
||||
|
||||
test("template click from home wipes any stale anonymous draft", async () => {
|
||||
window.localStorage.setItem(
|
||||
CREATE_FLOW_ANONYMOUS_KEY,
|
||||
JSON.stringify({ title: "Stale Community" }),
|
||||
);
|
||||
window.localStorage.setItem(
|
||||
CORE_VALUE_DETAILS_STORAGE_KEY,
|
||||
JSON.stringify({ "1": { meaning: "stale", signals: "stale" } }),
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(<RuleStack />);
|
||||
await waitForRuleStackCards();
|
||||
|
||||
const consensusCard = screen.getByText("Consensus").closest("div");
|
||||
await user.click(consensusCard);
|
||||
|
||||
expect(window.localStorage.getItem(CREATE_FLOW_ANONYMOUS_KEY)).toBeNull();
|
||||
expect(
|
||||
window.localStorage.getItem(CORE_VALUE_DETAILS_STORAGE_KEY),
|
||||
).toBeNull();
|
||||
|
||||
window.localStorage.clear();
|
||||
});
|
||||
|
||||
test("renders with proper semantic structure", async () => {
|
||||
render(<RuleStack />);
|
||||
await waitForRuleStackCards();
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildTemplateCustomizePrefill } from "../../lib/create/applyTemplatePrefill";
|
||||
import coreValuesMessages from "../../messages/en/create/customRule/coreValues.json";
|
||||
|
||||
function coreValuePresetId(label: string): string {
|
||||
const values = coreValuesMessages.values as Array<
|
||||
string | { label: string }
|
||||
>;
|
||||
const idx = values.findIndex((v) => {
|
||||
const l = typeof v === "string" ? v : v.label;
|
||||
return l.toLowerCase() === label.toLowerCase();
|
||||
});
|
||||
return String(idx + 1);
|
||||
}
|
||||
|
||||
describe("buildTemplateCustomizePrefill", () => {
|
||||
it("returns an empty object for malformed bodies", () => {
|
||||
expect(buildTemplateCustomizePrefill(null)).toEqual({});
|
||||
expect(buildTemplateCustomizePrefill({})).toEqual({});
|
||||
expect(buildTemplateCustomizePrefill({ sections: "nope" })).toEqual({});
|
||||
});
|
||||
|
||||
it("maps communication / membership / decisions / conflict titles to method-id slugs", () => {
|
||||
const body = {
|
||||
sections: [
|
||||
{
|
||||
categoryName: "Communication",
|
||||
entries: [
|
||||
{ title: "In-Person Meetings", body: "x" },
|
||||
{ title: "Loomio", body: "y" },
|
||||
],
|
||||
},
|
||||
{
|
||||
categoryName: "Membership",
|
||||
entries: [{ title: "Peer Sponsorship", body: "m" }],
|
||||
},
|
||||
{
|
||||
categoryName: "Decision-making",
|
||||
entries: [{ title: "Consensus Decision-Making", body: "d" }],
|
||||
},
|
||||
{
|
||||
categoryName: "Conflict management",
|
||||
entries: [{ title: "Restorative Justice", body: "c" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(buildTemplateCustomizePrefill(body)).toEqual({
|
||||
selectedCommunicationMethodIds: ["in-person-meetings", "loomio"],
|
||||
selectedMembershipMethodIds: ["peer-sponsorship"],
|
||||
selectedDecisionApproachIds: ["consensus-decision-making"],
|
||||
selectedConflictManagementIds: ["restorative-justice"],
|
||||
});
|
||||
});
|
||||
|
||||
it("matches template Values against the preset list and marks them selected", () => {
|
||||
const body = {
|
||||
sections: [
|
||||
{
|
||||
categoryName: "Values",
|
||||
entries: [
|
||||
{ title: "Consensus", body: "" },
|
||||
{ title: "Community Care", body: "" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const prefill = buildTemplateCustomizePrefill(body);
|
||||
const selected = prefill.selectedCoreValueIds ?? [];
|
||||
expect(selected).toContain(coreValuePresetId("Consensus"));
|
||||
expect(selected).toContain(coreValuePresetId("Community Care"));
|
||||
|
||||
const snapshot = prefill.coreValuesChipsSnapshot ?? [];
|
||||
const selectedRows = snapshot.filter((r) => r.state === "selected");
|
||||
expect(selectedRows.map((r) => r.label).sort()).toEqual([
|
||||
"Community Care",
|
||||
"Consensus",
|
||||
]);
|
||||
// Unmatched presets should still appear, as unselected, so the screen
|
||||
// renders the full chip list (the select screen reads the snapshot as-is).
|
||||
expect(snapshot.length).toBeGreaterThan(selectedRows.length);
|
||||
});
|
||||
|
||||
it("preserves bespoke template values as custom chip rows", () => {
|
||||
const body = {
|
||||
sections: [
|
||||
{
|
||||
categoryName: "Values",
|
||||
entries: [{ title: "Very Bespoke Thing", body: "" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
const prefill = buildTemplateCustomizePrefill(body);
|
||||
const custom = (prefill.coreValuesChipsSnapshot ?? []).find(
|
||||
(r) => r.label === "Very Bespoke Thing",
|
||||
);
|
||||
expect(custom).toBeDefined();
|
||||
expect(custom?.state).toBe("selected");
|
||||
expect(prefill.selectedCoreValueIds).toContain(custom?.id);
|
||||
});
|
||||
|
||||
it("ignores unknown category names", () => {
|
||||
const prefill = buildTemplateCustomizePrefill({
|
||||
sections: [
|
||||
{ categoryName: "Mystery", entries: [{ title: "What", body: "" }] },
|
||||
],
|
||||
});
|
||||
expect(prefill).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { buildFinalReviewCategoriesFromState } from "../../lib/create/buildFinalReviewCategories";
|
||||
import type { CreateFlowState } from "../../app/(app)/create/types";
|
||||
|
||||
const NAMES = {
|
||||
values: "Values",
|
||||
communication: "Communication",
|
||||
membership: "Membership",
|
||||
decisions: "Decision-making",
|
||||
conflict: "Conflict management",
|
||||
};
|
||||
|
||||
describe("buildFinalReviewCategoriesFromState", () => {
|
||||
it("returns [] when state has no selections and no sections", () => {
|
||||
expect(buildFinalReviewCategoriesFromState({}, NAMES)).toEqual([]);
|
||||
});
|
||||
|
||||
it("resolves method ids to labels from message presets", () => {
|
||||
// IDs here match `messages/en/create/customRule/*.json` `methods[].id`.
|
||||
// Same shape `buildTemplateCustomizePrefill` emits via methodSlugFromTitle.
|
||||
const state: CreateFlowState = {
|
||||
selectedCommunicationMethodIds: ["signal", "in-person-meetings"],
|
||||
selectedMembershipMethodIds: ["open-access"],
|
||||
selectedDecisionApproachIds: ["lazy-consensus"],
|
||||
selectedConflictManagementIds: ["peer-mediation"],
|
||||
};
|
||||
const rows = buildFinalReviewCategoriesFromState(state, NAMES);
|
||||
const byName = new Map(rows.map((r) => [r.name, r.chips]));
|
||||
expect(byName.get("Communication")).toEqual([
|
||||
"Signal",
|
||||
"In-Person Meetings",
|
||||
]);
|
||||
expect(byName.get("Membership")).toEqual(["Open Access"]);
|
||||
expect(byName.get("Decision-making")).toEqual(["Lazy Consensus"]);
|
||||
expect(byName.get("Conflict management")).toEqual(["Peer Mediation"]);
|
||||
expect(byName.has("Values")).toBe(false);
|
||||
});
|
||||
|
||||
it("derives core values via buildCoreValuesForDocument (snapshot + selected ids)", () => {
|
||||
const state: CreateFlowState = {
|
||||
selectedCoreValueIds: ["1", "custom-one"],
|
||||
coreValuesChipsSnapshot: [
|
||||
{ id: "1", label: "Accessibility", state: "selected" },
|
||||
{ id: "2", label: "Accountability", state: "unselected" },
|
||||
{ id: "custom-one", label: "Resilience", state: "selected" },
|
||||
],
|
||||
};
|
||||
const rows = buildFinalReviewCategoriesFromState(state, NAMES);
|
||||
expect(rows).toEqual([
|
||||
{ name: "Values", chips: ["Accessibility", "Resilience"] },
|
||||
]);
|
||||
});
|
||||
|
||||
it("drops unknown ids silently instead of inserting empty labels", () => {
|
||||
const state: CreateFlowState = {
|
||||
selectedCommunicationMethodIds: ["signal", "bogus-id"],
|
||||
};
|
||||
const rows = buildFinalReviewCategoriesFromState(state, NAMES);
|
||||
expect(rows).toEqual([{ name: "Communication", chips: ["Signal"] }]);
|
||||
});
|
||||
|
||||
it("dedupes repeated labels from duplicate ids", () => {
|
||||
const state: CreateFlowState = {
|
||||
selectedCommunicationMethodIds: ["signal", "signal"],
|
||||
};
|
||||
const rows = buildFinalReviewCategoriesFromState(state, NAMES);
|
||||
expect(rows).toEqual([{ name: "Communication", chips: ["Signal"] }]);
|
||||
});
|
||||
|
||||
it("prefers state.sections when populated (use-without-changes path)", () => {
|
||||
const state: CreateFlowState = {
|
||||
sections: [
|
||||
{
|
||||
categoryName: "Values",
|
||||
entries: [
|
||||
{ title: "Consciousness", body: "…" },
|
||||
{ title: "Ecology", body: "…" },
|
||||
],
|
||||
},
|
||||
{
|
||||
categoryName: "Communication",
|
||||
entries: [{ title: "Signal", body: "…" }],
|
||||
},
|
||||
],
|
||||
// Selection ids must be ignored when sections is present — the
|
||||
// "Use without changes" handler resets them for exactly that reason,
|
||||
// but we double-check the helper honors the sections branch first.
|
||||
selectedCommunicationMethodIds: ["in-person-meetings"],
|
||||
};
|
||||
const rows = buildFinalReviewCategoriesFromState(state, NAMES);
|
||||
expect(rows).toEqual([
|
||||
{ name: "Values", chips: ["Consciousness", "Ecology"] },
|
||||
{ name: "Communication", chips: ["Signal"] },
|
||||
]);
|
||||
});
|
||||
|
||||
it("prepends a Values row from coreValuesChipsSnapshot when sections lack one", () => {
|
||||
const state: CreateFlowState = {
|
||||
sections: [
|
||||
{
|
||||
categoryName: "Communication",
|
||||
entries: [{ title: "Signal", body: "…" }],
|
||||
},
|
||||
],
|
||||
selectedCoreValueIds: ["1"],
|
||||
coreValuesChipsSnapshot: [
|
||||
{ id: "1", label: "Accessibility", state: "selected" },
|
||||
],
|
||||
};
|
||||
const rows = buildFinalReviewCategoriesFromState(state, NAMES);
|
||||
expect(rows).toEqual([
|
||||
{ name: "Values", chips: ["Accessibility"] },
|
||||
{ name: "Communication", chips: ["Signal"] },
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not duplicate Values when sections already includes one", () => {
|
||||
const state: CreateFlowState = {
|
||||
sections: [
|
||||
{
|
||||
categoryName: "Values",
|
||||
entries: [{ title: "Consciousness", body: "…" }],
|
||||
},
|
||||
],
|
||||
selectedCoreValueIds: ["1"],
|
||||
coreValuesChipsSnapshot: [
|
||||
{ id: "1", label: "Accessibility", state: "selected" },
|
||||
],
|
||||
};
|
||||
const rows = buildFinalReviewCategoriesFromState(state, NAMES);
|
||||
expect(rows).toEqual([
|
||||
{ name: "Values", chips: ["Consciousness"] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,38 +0,0 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { hasCreateFlowUserInput } from "../../app/(app)/create/utils/hasCreateFlowUserInput";
|
||||
|
||||
describe("hasCreateFlowUserInput", () => {
|
||||
it("returns false for empty state", () => {
|
||||
expect(hasCreateFlowUserInput({})).toBe(false);
|
||||
});
|
||||
|
||||
it("ignores currentStep alone", () => {
|
||||
expect(hasCreateFlowUserInput({ currentStep: "informational" })).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns true for non-empty title", () => {
|
||||
expect(hasCreateFlowUserInput({ title: "My rule" })).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for whitespace-only title", () => {
|
||||
expect(hasCreateFlowUserInput({ title: " " })).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true for non-empty sections array", () => {
|
||||
expect(hasCreateFlowUserInput({ sections: [{}] })).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for empty sections array", () => {
|
||||
expect(hasCreateFlowUserInput({ sections: [] })).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true for extra step-specific keys with content", () => {
|
||||
expect(hasCreateFlowUserInput({ cards: ["a"] })).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for extra keys with empty object", () => {
|
||||
expect(hasCreateFlowUserInput({ foo: {} })).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user