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");
});
});