Template flow cleaned up

This commit is contained in:
adilallo
2026-04-20 16:45:15 -06:00
parent d3bb8cdd0f
commit c08cd62872
32 changed files with 1545 additions and 254 deletions
+43
View File
@@ -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();
});
});
+76 -7
View File
@@ -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();
});
});
+52 -3
View File
@@ -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");
});
});
+111
View File
@@ -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("");
});
});
+66 -1
View File
@@ -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",
);
});
});
+27
View File
@@ -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();
+109
View File
@@ -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"] },
]);
});
});
-38
View File
@@ -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);
});
});