From 427dc4447660693375fe013c4b91256c3f757006 Mon Sep 17 00:00:00 2001
From: adilallo <39313955+adilallo@users.noreply.github.com>
Date: Sat, 4 Apr 2026 10:57:01 -0600
Subject: [PATCH] Cleanup, add tests and storybook
---
.../WebVitalsDashboard.container.tsx | 3 +-
stories/pages/CardsPage.stories.js | 35 +++++++++++++
stories/pages/CompletedPage.stories.js | 35 +++++++++++++
.../pages/ConfirmStakeholdersPage.stories.js | 35 +++++++++++++
stories/pages/FinalReviewPage.stories.js | 35 +++++++++++++
stories/pages/InformationalPage.stories.js | 35 +++++++++++++
stories/pages/RightRailPage.stories.js | 35 +++++++++++++
stories/pages/SelectPage.stories.js | 35 +++++++++++++
stories/pages/TextPage.stories.js | 35 +++++++++++++
stories/pages/UploadPage.stories.js | 35 +++++++++++++
.../utility/DecisionMakingSidebar.stories.js | 52 +++++++++++++++++++
stories/utility/InfoMessageBox.stories.js | 43 +++++++++++++++
tests/components/CompletedPage.test.tsx | 1 -
.../ConfirmStakeholdersPage.test.tsx | 40 ++++++++++++++
tests/components/ContentBanner.test.tsx | 4 ++
.../components/DecisionMakingSidebar.test.tsx | 38 ++++++++++++++
tests/components/FinalReviewPage.test.tsx | 1 -
tests/components/InfoMessageBox.test.tsx | 37 +++++++++++++
tests/components/InformationalPage.test.tsx | 31 +++++++++++
tests/components/RelatedArticles.test.tsx | 8 +++
tests/components/ReviewPage.test.tsx | 1 -
tests/components/SelectPage.test.tsx | 29 +++++++++++
tests/components/TextPage.test.tsx | 25 +++++++++
tests/components/UploadPage.test.tsx | 23 ++++++++
tests/unit/flowSteps.test.ts | 49 +++++++++++++++++
vitest.setup.ts | 7 +--
26 files changed, 700 insertions(+), 7 deletions(-)
create mode 100644 stories/pages/CardsPage.stories.js
create mode 100644 stories/pages/CompletedPage.stories.js
create mode 100644 stories/pages/ConfirmStakeholdersPage.stories.js
create mode 100644 stories/pages/FinalReviewPage.stories.js
create mode 100644 stories/pages/InformationalPage.stories.js
create mode 100644 stories/pages/RightRailPage.stories.js
create mode 100644 stories/pages/SelectPage.stories.js
create mode 100644 stories/pages/TextPage.stories.js
create mode 100644 stories/pages/UploadPage.stories.js
create mode 100644 stories/utility/DecisionMakingSidebar.stories.js
create mode 100644 stories/utility/InfoMessageBox.stories.js
create mode 100644 tests/components/ConfirmStakeholdersPage.test.tsx
create mode 100644 tests/components/DecisionMakingSidebar.test.tsx
create mode 100644 tests/components/InfoMessageBox.test.tsx
create mode 100644 tests/components/InformationalPage.test.tsx
create mode 100644 tests/components/SelectPage.test.tsx
create mode 100644 tests/components/TextPage.test.tsx
create mode 100644 tests/components/UploadPage.test.tsx
create mode 100644 tests/unit/flowSteps.test.ts
diff --git a/app/components/WebVitalsDashboard/WebVitalsDashboard.container.tsx b/app/components/WebVitalsDashboard/WebVitalsDashboard.container.tsx
index dafa943..a5caf4e 100644
--- a/app/components/WebVitalsDashboard/WebVitalsDashboard.container.tsx
+++ b/app/components/WebVitalsDashboard/WebVitalsDashboard.container.tsx
@@ -40,7 +40,8 @@ const WebVitalsDashboardContainer = memo(() => {
if (typeof window !== "undefined") {
import("web-vitals").then((webVitals) => {
- const { getCLS, getFID, getFCP, getLCP, getTTFB } = webVitals as {
+ // web-vitals v4 typings don't expose legacy get* names the same way; runtime bundle still provides them for this dashboard.
+ const { getCLS, getFID, getFCP, getLCP, getTTFB } = webVitals as unknown as {
getCLS: (
_fn: (_m: { value: number; rating: string }) => void,
) => void;
diff --git a/stories/pages/CardsPage.stories.js b/stories/pages/CardsPage.stories.js
new file mode 100644
index 0000000..c40fef8
--- /dev/null
+++ b/stories/pages/CardsPage.stories.js
@@ -0,0 +1,35 @@
+import CardsPage from "../../app/create/cards/page";
+
+export default {
+ title: "Pages/Create Flow/Cards",
+ component: CardsPage,
+ parameters: {
+ layout: "fullscreen",
+ docs: {
+ description: {
+ component:
+ "Communication / card selection step with modals and responsive layout.",
+ },
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ tags: ["autodocs"],
+};
+
+export const Desktop = {
+ parameters: {
+ viewport: { defaultViewport: "desktop" },
+ },
+};
+
+export const Mobile = {
+ parameters: {
+ viewport: { defaultViewport: "mobile1" },
+ },
+};
diff --git a/stories/pages/CompletedPage.stories.js b/stories/pages/CompletedPage.stories.js
new file mode 100644
index 0000000..fbeb474
--- /dev/null
+++ b/stories/pages/CompletedPage.stories.js
@@ -0,0 +1,35 @@
+import CompletedPage from "../../app/create/completed/page";
+
+export default {
+ title: "Pages/Create Flow/Completed",
+ component: CompletedPage,
+ parameters: {
+ layout: "fullscreen",
+ docs: {
+ description: {
+ component:
+ "Completed flow: teal background, inverse HeaderLockup, CommunityRule document, optional bottom toast.",
+ },
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ tags: ["autodocs"],
+};
+
+export const Desktop = {
+ parameters: {
+ viewport: { defaultViewport: "desktop" },
+ },
+};
+
+export const Mobile = {
+ parameters: {
+ viewport: { defaultViewport: "mobile1" },
+ },
+};
diff --git a/stories/pages/ConfirmStakeholdersPage.stories.js b/stories/pages/ConfirmStakeholdersPage.stories.js
new file mode 100644
index 0000000..0f38732
--- /dev/null
+++ b/stories/pages/ConfirmStakeholdersPage.stories.js
@@ -0,0 +1,35 @@
+import ConfirmStakeholdersPage from "../../app/create/confirm-stakeholders/page";
+
+export default {
+ title: "Pages/Create Flow/Confirm stakeholders",
+ component: ConfirmStakeholdersPage,
+ parameters: {
+ layout: "fullscreen",
+ docs: {
+ description: {
+ component:
+ "Stacked lockup + MultiSelect; draft congratulations banner; before final review.",
+ },
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ tags: ["autodocs"],
+};
+
+export const Desktop = {
+ parameters: {
+ viewport: { defaultViewport: "desktop" },
+ },
+};
+
+export const Mobile = {
+ parameters: {
+ viewport: { defaultViewport: "mobile1" },
+ },
+};
diff --git a/stories/pages/FinalReviewPage.stories.js b/stories/pages/FinalReviewPage.stories.js
new file mode 100644
index 0000000..77d4bc4
--- /dev/null
+++ b/stories/pages/FinalReviewPage.stories.js
@@ -0,0 +1,35 @@
+import FinalReviewPage from "../../app/create/final-review/page";
+
+export default {
+ title: "Pages/Create Flow/Final review",
+ component: FinalReviewPage,
+ parameters: {
+ layout: "fullscreen",
+ docs: {
+ description: {
+ component:
+ "Pre-finalize review: HeaderLockup + expanded RuleCard sections.",
+ },
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ tags: ["autodocs"],
+};
+
+export const Desktop = {
+ parameters: {
+ viewport: { defaultViewport: "desktop" },
+ },
+};
+
+export const Mobile = {
+ parameters: {
+ viewport: { defaultViewport: "mobile1" },
+ },
+};
diff --git a/stories/pages/InformationalPage.stories.js b/stories/pages/InformationalPage.stories.js
new file mode 100644
index 0000000..7b0d9ab
--- /dev/null
+++ b/stories/pages/InformationalPage.stories.js
@@ -0,0 +1,35 @@
+import InformationalPage from "../../app/create/informational/page";
+
+export default {
+ title: "Pages/Create Flow/Informational",
+ component: InformationalPage,
+ parameters: {
+ layout: "fullscreen",
+ docs: {
+ description: {
+ component:
+ "Create flow entry: HeaderLockup + NumberedList. Responsive L/M and M/S at 640px.",
+ },
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ tags: ["autodocs"],
+};
+
+export const Desktop = {
+ parameters: {
+ viewport: { defaultViewport: "desktop" },
+ },
+};
+
+export const Mobile = {
+ parameters: {
+ viewport: { defaultViewport: "mobile1" },
+ },
+};
diff --git a/stories/pages/RightRailPage.stories.js b/stories/pages/RightRailPage.stories.js
new file mode 100644
index 0000000..772e1bc
--- /dev/null
+++ b/stories/pages/RightRailPage.stories.js
@@ -0,0 +1,35 @@
+import RightRailPage from "../../app/create/right-rail/page";
+
+export default {
+ title: "Pages/Create Flow/Right rail",
+ component: RightRailPage,
+ parameters: {
+ layout: "fullscreen",
+ docs: {
+ description: {
+ component:
+ "Decision-making sidebar layout with CardStack and supporting content.",
+ },
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ tags: ["autodocs"],
+};
+
+export const Desktop = {
+ parameters: {
+ viewport: { defaultViewport: "desktop" },
+ },
+};
+
+export const Mobile = {
+ parameters: {
+ viewport: { defaultViewport: "mobile1" },
+ },
+};
diff --git a/stories/pages/SelectPage.stories.js b/stories/pages/SelectPage.stories.js
new file mode 100644
index 0000000..86e69e5
--- /dev/null
+++ b/stories/pages/SelectPage.stories.js
@@ -0,0 +1,35 @@
+import SelectPage from "../../app/create/select/page";
+
+export default {
+ title: "Pages/Create Flow/Select",
+ component: SelectPage,
+ parameters: {
+ layout: "fullscreen",
+ docs: {
+ description: {
+ component:
+ "Multi-select template: two columns at 640px+, stacked below. MultiSelect with add → custom chip.",
+ },
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ tags: ["autodocs"],
+};
+
+export const Desktop = {
+ parameters: {
+ viewport: { defaultViewport: "desktop" },
+ },
+};
+
+export const Mobile = {
+ parameters: {
+ viewport: { defaultViewport: "mobile1" },
+ },
+};
diff --git a/stories/pages/TextPage.stories.js b/stories/pages/TextPage.stories.js
new file mode 100644
index 0000000..15debca
--- /dev/null
+++ b/stories/pages/TextPage.stories.js
@@ -0,0 +1,35 @@
+import TextPage from "../../app/create/text/page";
+
+export default {
+ title: "Pages/Create Flow/Text",
+ component: TextPage,
+ parameters: {
+ layout: "fullscreen",
+ docs: {
+ description: {
+ component:
+ "Community name step: HeaderLockup + TextInput. Responsive sizing at 640px.",
+ },
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ tags: ["autodocs"],
+};
+
+export const Desktop = {
+ parameters: {
+ viewport: { defaultViewport: "desktop" },
+ },
+};
+
+export const Mobile = {
+ parameters: {
+ viewport: { defaultViewport: "mobile1" },
+ },
+};
diff --git a/stories/pages/UploadPage.stories.js b/stories/pages/UploadPage.stories.js
new file mode 100644
index 0000000..4736497
--- /dev/null
+++ b/stories/pages/UploadPage.stories.js
@@ -0,0 +1,35 @@
+import UploadPage from "../../app/create/upload/page";
+
+export default {
+ title: "Pages/Create Flow/Upload",
+ component: UploadPage,
+ parameters: {
+ layout: "fullscreen",
+ docs: {
+ description: {
+ component:
+ "Upload step: HeaderLockup + Upload control. Centered lockup at 640px+.",
+ },
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ tags: ["autodocs"],
+};
+
+export const Desktop = {
+ parameters: {
+ viewport: { defaultViewport: "desktop" },
+ },
+};
+
+export const Mobile = {
+ parameters: {
+ viewport: { defaultViewport: "mobile1" },
+ },
+};
diff --git a/stories/utility/DecisionMakingSidebar.stories.js b/stories/utility/DecisionMakingSidebar.stories.js
new file mode 100644
index 0000000..0ddcc7e
--- /dev/null
+++ b/stories/utility/DecisionMakingSidebar.stories.js
@@ -0,0 +1,52 @@
+import DecisionMakingSidebar from "../../app/components/utility/DecisionMakingSidebar";
+
+export default {
+ title: "Components/Utility/DecisionMakingSidebar",
+ component: DecisionMakingSidebar,
+ parameters: {
+ layout: "centered",
+ backgrounds: { default: "dark" },
+ docs: {
+ description: {
+ component:
+ "HeaderLockup + InfoMessageBox for decision-making step sidebars.",
+ },
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ tags: ["autodocs"],
+};
+
+const messageItems = [
+ { id: "c1", label: "Consensus" },
+ { id: "c2", label: "Majority vote" },
+];
+
+export const Default = {
+ args: {
+ title: "How does your group make decisions?",
+ description:
+ "Choose the approaches that best match how your community operates.",
+ messageBoxTitle: "Common approaches",
+ messageBoxItems: messageItems,
+ size: "L",
+ justification: "left",
+ },
+};
+
+export const Medium = {
+ args: {
+ title: "Decision-making",
+ description: "Short description.",
+ messageBoxTitle: "Pick any",
+ messageBoxItems: [{ id: "x", label: "Single method" }],
+ size: "M",
+ justification: "left",
+ },
+};
diff --git a/stories/utility/InfoMessageBox.stories.js b/stories/utility/InfoMessageBox.stories.js
new file mode 100644
index 0000000..725807d
--- /dev/null
+++ b/stories/utility/InfoMessageBox.stories.js
@@ -0,0 +1,43 @@
+import InfoMessageBox from "../../app/components/utility/InfoMessageBox";
+
+export default {
+ title: "Components/Utility/InfoMessageBox",
+ component: InfoMessageBox,
+ parameters: {
+ layout: "centered",
+ backgrounds: { default: "dark" },
+ docs: {
+ description: {
+ component:
+ "Message region with optional exclamation icon and CheckboxGroup items.",
+ },
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ tags: ["autodocs"],
+};
+
+const sampleItems = [
+ { id: "1", label: "First option" },
+ { id: "2", label: "Second option" },
+];
+
+export const Default = {
+ args: {
+ title: "Before you continue",
+ items: sampleItems,
+ },
+};
+
+export const SingleItem = {
+ args: {
+ title: "Select one",
+ items: [{ id: "a", label: "Only choice" }],
+ },
+};
diff --git a/tests/components/CompletedPage.test.tsx b/tests/components/CompletedPage.test.tsx
index 51d8ed2..e12f201 100644
--- a/tests/components/CompletedPage.test.tsx
+++ b/tests/components/CompletedPage.test.tsx
@@ -1,4 +1,3 @@
-import React from "react";
import { describe, it, expect } from "vitest";
import { renderWithProviders as render, screen } from "../utils/test-utils";
import "@testing-library/jest-dom/vitest";
diff --git a/tests/components/ConfirmStakeholdersPage.test.tsx b/tests/components/ConfirmStakeholdersPage.test.tsx
new file mode 100644
index 0000000..8357d80
--- /dev/null
+++ b/tests/components/ConfirmStakeholdersPage.test.tsx
@@ -0,0 +1,40 @@
+import { describe, it, expect } from "vitest";
+import userEvent from "@testing-library/user-event";
+import { renderWithProviders as render, screen } from "../utils/test-utils";
+import "@testing-library/jest-dom/vitest";
+import ConfirmStakeholdersPage from "../../app/create/confirm-stakeholders/page";
+
+describe("ConfirmStakeholdersPage", () => {
+ it("renders title and description", () => {
+ render();
+ expect(
+ screen.getByRole("heading", {
+ name: /Do other stakeholders need to be involved/i,
+ }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ /Adding people at this step will invite them to see your proposed CommunityRule/i,
+ ),
+ ).toBeInTheDocument();
+ });
+
+ it("renders Add stakeholder control", () => {
+ render();
+ expect(
+ screen.getByRole("button", { name: "Add stakeholder" }),
+ ).toBeInTheDocument();
+ });
+
+ it("shows draft toast and can dismiss it", async () => {
+ const user = userEvent.setup();
+ render();
+ expect(
+ screen.getByText(/Congratulations! You've drafted your CommunityRule!/i),
+ ).toBeInTheDocument();
+ await user.click(screen.getByRole("button", { name: "Close alert" }));
+ expect(
+ screen.queryByText(/Congratulations! You've drafted your CommunityRule!/i),
+ ).not.toBeInTheDocument();
+ });
+});
diff --git a/tests/components/ContentBanner.test.tsx b/tests/components/ContentBanner.test.tsx
index b15b54d..0c73395 100644
--- a/tests/components/ContentBanner.test.tsx
+++ b/tests/components/ContentBanner.test.tsx
@@ -37,6 +37,10 @@ const mockPost: BlogPost = {
author: "Test Author",
date: "2025-04-15",
},
+ content: "",
+ htmlContent: "",
+ filePath: "test-article.md",
+ lastModified: new Date("2025-04-15"),
};
describe("ContentBanner", () => {
diff --git a/tests/components/DecisionMakingSidebar.test.tsx b/tests/components/DecisionMakingSidebar.test.tsx
new file mode 100644
index 0000000..c5bde54
--- /dev/null
+++ b/tests/components/DecisionMakingSidebar.test.tsx
@@ -0,0 +1,38 @@
+import { describe, it, expect } from "vitest";
+import { renderWithProviders as render, screen } from "../utils/test-utils";
+import "@testing-library/jest-dom/vitest";
+import DecisionMakingSidebar from "../../app/components/utility/DecisionMakingSidebar";
+
+describe("DecisionMakingSidebar", () => {
+ const messageBoxItems = [{ id: "1", label: "Consensus" }];
+
+ it("renders title and description", () => {
+ render(
+ ,
+ );
+ expect(
+ screen.getByRole("heading", { name: "How are decisions made?" }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText("Pick approaches for your group."),
+ ).toBeInTheDocument();
+ });
+
+ it("renders InfoMessageBox section", () => {
+ render(
+ ,
+ );
+ expect(screen.getByText("Select methods")).toBeInTheDocument();
+ expect(screen.getByText("Consensus")).toBeInTheDocument();
+ });
+});
diff --git a/tests/components/FinalReviewPage.test.tsx b/tests/components/FinalReviewPage.test.tsx
index 2395f39..52ace8d 100644
--- a/tests/components/FinalReviewPage.test.tsx
+++ b/tests/components/FinalReviewPage.test.tsx
@@ -1,4 +1,3 @@
-import React from "react";
import { describe, it, expect } from "vitest";
import { renderWithProviders as render, screen } from "../utils/test-utils";
import "@testing-library/jest-dom/vitest";
diff --git a/tests/components/InfoMessageBox.test.tsx b/tests/components/InfoMessageBox.test.tsx
new file mode 100644
index 0000000..5a2964d
--- /dev/null
+++ b/tests/components/InfoMessageBox.test.tsx
@@ -0,0 +1,37 @@
+import { describe, it, expect, vi } from "vitest";
+import userEvent from "@testing-library/user-event";
+import { renderWithProviders as render, screen } from "../utils/test-utils";
+import "@testing-library/jest-dom/vitest";
+import InfoMessageBox from "../../app/components/utility/InfoMessageBox";
+
+describe("InfoMessageBox", () => {
+ const items = [
+ { id: "a", label: "Option A" },
+ { id: "b", label: "Option B" },
+ ];
+
+ it("renders title and item labels", () => {
+ render(
+ ,
+ );
+ expect(screen.getByRole("region", { name: "Important" })).toBeInTheDocument();
+ expect(screen.getByText("Important")).toBeInTheDocument();
+ expect(screen.getByText("Option A")).toBeInTheDocument();
+ expect(screen.getByText("Option B")).toBeInTheDocument();
+ });
+
+ it("calls onCheckboxChange when toggling", async () => {
+ const u = userEvent.setup();
+ const onCheckboxChange = vi.fn();
+ render(
+ ,
+ );
+ const checkbox = screen.getByRole("checkbox", { name: /Choice X/i });
+ await u.click(checkbox);
+ expect(onCheckboxChange).toHaveBeenCalled();
+ });
+});
diff --git a/tests/components/InformationalPage.test.tsx b/tests/components/InformationalPage.test.tsx
new file mode 100644
index 0000000..9e8d50d
--- /dev/null
+++ b/tests/components/InformationalPage.test.tsx
@@ -0,0 +1,31 @@
+import { describe, it, expect } from "vitest";
+import { renderWithProviders as render, screen } from "../utils/test-utils";
+import "@testing-library/jest-dom/vitest";
+import InformationalPage from "../../app/create/informational/page";
+
+describe("InformationalPage", () => {
+ it("renders without crashing", () => {
+ render();
+ expect(
+ screen.getByRole("heading", {
+ name: "How CommunityRule helps groups like yours",
+ }),
+ ).toBeInTheDocument();
+ });
+
+ it("renders lockup description", () => {
+ render();
+ expect(
+ screen.getByText(
+ /This flow will give you recommendations to improve your community/i,
+ ),
+ ).toBeInTheDocument();
+ });
+
+ it("renders first numbered list item title", () => {
+ render();
+ expect(
+ screen.getByText("Tell us about your organization"),
+ ).toBeInTheDocument();
+ });
+});
diff --git a/tests/components/RelatedArticles.test.tsx b/tests/components/RelatedArticles.test.tsx
index aa78d8b..65a2016 100644
--- a/tests/components/RelatedArticles.test.tsx
+++ b/tests/components/RelatedArticles.test.tsx
@@ -43,6 +43,10 @@ const mockPosts: BlogPost[] = [
author: "Author",
date: "2025-04-10",
},
+ content: "",
+ htmlContent: "",
+ filePath: "article-1.md",
+ lastModified: new Date("2025-04-10"),
},
{
slug: "article-2",
@@ -52,6 +56,10 @@ const mockPosts: BlogPost[] = [
author: "Author",
date: "2025-04-11",
},
+ content: "",
+ htmlContent: "",
+ filePath: "article-2.md",
+ lastModified: new Date("2025-04-11"),
},
];
diff --git a/tests/components/ReviewPage.test.tsx b/tests/components/ReviewPage.test.tsx
index c35c5ea..3569303 100644
--- a/tests/components/ReviewPage.test.tsx
+++ b/tests/components/ReviewPage.test.tsx
@@ -1,4 +1,3 @@
-import React from "react";
import { describe, it, expect } from "vitest";
import { renderWithProviders as render, screen } from "../utils/test-utils";
import "@testing-library/jest-dom/vitest";
diff --git a/tests/components/SelectPage.test.tsx b/tests/components/SelectPage.test.tsx
new file mode 100644
index 0000000..0f0439b
--- /dev/null
+++ b/tests/components/SelectPage.test.tsx
@@ -0,0 +1,29 @@
+import { describe, it, expect } from "vitest";
+import { renderWithProviders as render, screen } from "../utils/test-utils";
+import "@testing-library/jest-dom/vitest";
+import SelectPage from "../../app/create/select/page";
+
+describe("SelectPage", () => {
+ it("renders HeaderLockup title", () => {
+ render();
+ expect(
+ screen.getByRole("heading", {
+ name: "What is your community called?",
+ }),
+ ).toBeInTheDocument();
+ });
+
+ it("renders MultiSelect add control", () => {
+ render();
+ const addButtons = screen.getAllByRole("button", {
+ name: "Add organization type",
+ });
+ expect(addButtons.length).toBeGreaterThanOrEqual(1);
+ });
+
+ it("renders preset chip labels", () => {
+ render();
+ expect(screen.getByText("1 member")).toBeInTheDocument();
+ expect(screen.getByText("Non-profit")).toBeInTheDocument();
+ });
+});
diff --git a/tests/components/TextPage.test.tsx b/tests/components/TextPage.test.tsx
new file mode 100644
index 0000000..b37b0b2
--- /dev/null
+++ b/tests/components/TextPage.test.tsx
@@ -0,0 +1,25 @@
+import { describe, it, expect } from "vitest";
+import { renderWithProviders as render, screen } from "../utils/test-utils";
+import "@testing-library/jest-dom/vitest";
+import TextPage from "../../app/create/text/page";
+
+describe("TextPage", () => {
+ it("renders main heading", () => {
+ render();
+ expect(
+ screen.getByRole("heading", {
+ name: "What is your community called?",
+ }),
+ ).toBeInTheDocument();
+ });
+
+ it("renders description and text field", () => {
+ render();
+ expect(
+ screen.getByText("This will be the name of your community"),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByPlaceholderText("Enter your community name"),
+ ).toBeInTheDocument();
+ });
+});
diff --git a/tests/components/UploadPage.test.tsx b/tests/components/UploadPage.test.tsx
new file mode 100644
index 0000000..91d63b4
--- /dev/null
+++ b/tests/components/UploadPage.test.tsx
@@ -0,0 +1,23 @@
+import { describe, it, expect } from "vitest";
+import { renderWithProviders as render, screen } from "../utils/test-utils";
+import "@testing-library/jest-dom/vitest";
+import UploadPage from "../../app/create/upload/page";
+
+describe("UploadPage", () => {
+ it("renders HeaderLockup", () => {
+ render();
+ expect(
+ screen.getByRole("heading", {
+ name: "How should conflicts be resolved?",
+ }),
+ ).toBeInTheDocument();
+ });
+
+ it("renders Upload control and helper copy", () => {
+ render();
+ expect(screen.getByRole("button", { name: "Upload" })).toBeInTheDocument();
+ expect(
+ screen.getByText(/Add images, PDFs, and other files to the policy/i),
+ ).toBeInTheDocument();
+ });
+});
diff --git a/tests/unit/flowSteps.test.ts b/tests/unit/flowSteps.test.ts
new file mode 100644
index 0000000..47e5675
--- /dev/null
+++ b/tests/unit/flowSteps.test.ts
@@ -0,0 +1,49 @@
+import { describe, it, expect } from "vitest";
+import {
+ FLOW_STEP_ORDER,
+ getNextStep,
+ getPreviousStep,
+ isValidStep,
+ getStepIndex,
+} from "../../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");
+ expect(i).toBeGreaterThanOrEqual(0);
+ expect(j).toBe(i + 1);
+ });
+
+ it("getNextStep returns next step in order", () => {
+ expect(getNextStep("right-rail")).toBe("confirm-stakeholders");
+ expect(getNextStep("confirm-stakeholders")).toBe("final-review");
+ });
+
+ it("getNextStep returns null for last step or invalid", () => {
+ expect(getNextStep("completed")).toBeNull();
+ expect(getNextStep(null)).toBeNull();
+ // @ts-expect-error — exercise invalid step id at runtime
+ expect(getNextStep("not-a-step")).toBeNull();
+ });
+
+ it("getPreviousStep returns prior step or null", () => {
+ expect(getPreviousStep("final-review")).toBe("confirm-stakeholders");
+ expect(getPreviousStep("informational")).toBeNull();
+ expect(getPreviousStep(null)).toBeNull();
+ });
+
+ it("isValidStep reflects FLOW_STEP_ORDER membership", () => {
+ expect(isValidStep("select")).toBe(true);
+ expect(isValidStep("confirm-stakeholders")).toBe(true);
+ expect(isValidStep("nope")).toBe(false);
+ expect(isValidStep(null)).toBe(false);
+ });
+
+ it("getStepIndex matches position in FLOW_STEP_ORDER", () => {
+ expect(getStepIndex("informational")).toBe(0);
+ expect(getStepIndex("completed")).toBe(FLOW_STEP_ORDER.length - 1);
+ // @ts-expect-error — invalid step id
+ expect(getStepIndex("bogus")).toBe(-1);
+ });
+});
diff --git a/vitest.setup.ts b/vitest.setup.ts
index 6b22350..a265b36 100644
--- a/vitest.setup.ts
+++ b/vitest.setup.ts
@@ -1,5 +1,5 @@
import "@testing-library/jest-dom/vitest";
-import type React from "react";
+import React from "react";
import { afterAll, afterEach, beforeAll, vi } from "vitest";
import { cleanup } from "@testing-library/react";
import { server } from "./tests/msw/server";
@@ -18,8 +18,9 @@ vi.mock("next/dynamic", () => {
) => {
// In tests, return a component that immediately resolves and renders
return function DynamicComponent(props: Record) {
- const [Component, setComponent] =
- React.useState(null);
+ const [Component, setComponent] = React.useState(
+ null as React.ComponentType | null,
+ );
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {