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(() => {