Full cleanup pass

This commit is contained in:
adilallo
2026-05-21 23:25:56 -06:00
parent 28de8ef3bc
commit 99f535f821
149 changed files with 2623 additions and 1242 deletions
+4 -2
View File
@@ -45,6 +45,7 @@ vi.mock("next/dynamic", () => {
vi.mock("../../lib/content", () => ({
getBlogPostBySlug: vi.fn(),
getAllBlogPosts: vi.fn(),
getRelatedBlogPosts: vi.fn(() => []),
}));
// Mock components
@@ -137,10 +138,11 @@ describe("BlogPostPage", () => {
vi.clearAllMocks();
// Mock the content functions
const { getBlogPostBySlug, getAllBlogPosts } =
const { getBlogPostBySlug, getAllBlogPosts, getRelatedBlogPosts } =
await import("../../lib/content");
vi.mocked(getBlogPostBySlug).mockReturnValue(mockPost);
vi.mocked(getAllBlogPosts).mockReturnValue([mockPost, ...mockRelatedPosts]);
vi.mocked(getRelatedBlogPosts).mockReturnValue(mockRelatedPosts);
});
it("renders the blog post page with correct structure", async () => {
@@ -155,7 +157,7 @@ describe("BlogPostPage", () => {
expect(mainContainer).toHaveClass(
"min-h-screen",
"relative",
"overflow-hidden",
"overflow-x-clip",
);
// Background color is applied via inline style from frontmatter hex
expect(mainContainer).toHaveStyle({ backgroundColor: expect.any(String) });
+49 -80
View File
@@ -9,37 +9,22 @@ import { vi, describe, test, expect, afterEach } from "vitest";
import React from "react";
import Page from "../../app/(marketing)/page";
// Mock next/dynamic to return components synchronously in tests
vi.mock("next/dynamic", () => {
return {
default: (importFn) => {
// In tests, return the component directly by importing it synchronously
// This bypasses the async loading behavior for testing
return (props) => {
const [Component, setComponent] = React.useState(null);
React.useEffect(() => {
importFn().then((mod) => {
setComponent(() => mod.default || mod);
});
}, []);
if (!Component) {
return null;
}
return <Component {...props} />;
};
},
};
vi.mock("next/dynamic", async () => {
const { default: syncDynamic } = await import("../utils/mockNextDynamicSync.js");
return { default: syncDynamic };
});
function renderPage(ui = <Page />) {
return render(ui);
}
afterEach(() => {
cleanup();
});
describe("Page Flow Integration", () => {
// TODO: Fix next/dynamic mock to properly handle async component loading
// The mock currently doesn't resolve components synchronously, causing this test to fail
test.skip("renders complete page with all sections in correct order", async () => {
render(<Page />);
test("renders complete page with all sections in correct order", async () => {
renderPage();
// Hero Banner section
expect(
@@ -53,24 +38,20 @@ describe("Page Flow Integration", () => {
"Help your community make important decisions in a way that reflects its unique values.",
),
).toBeInTheDocument();
// Check that CTA button exists (multiple sizes for responsive design)
const ctaButtons = screen.getAllByRole("button", {
const ctaButtons = screen.getAllByRole("link", {
name: "Learn how CommunityRule works",
});
expect(ctaButtons.length).toBeGreaterThan(0);
// Wait for dynamically imported LogoWall component to load
await waitFor(() => {
expect(screen.getByAltText("Food Not Bombs")).toBeInTheDocument();
});
// Once LogoWall is loaded, other logos should be available
expect(screen.getByAltText("Start COOP")).toBeInTheDocument();
expect(screen.getByAltText("Metagov")).toBeInTheDocument();
expect(screen.getByAltText("Open Civics")).toBeInTheDocument();
expect(screen.getByAltText("Mutual Aid CO")).toBeInTheDocument();
expect(screen.getByAltText("CU Boulder")).toBeInTheDocument();
// CardSteps section — wait for dynamically imported component
await waitFor(() => {
expect(
screen.getByRole("heading", { name: /How CommunityRule works/ }),
@@ -93,8 +74,9 @@ describe("Page Flow Integration", () => {
),
).toBeInTheDocument();
// Rule Stack section
expect(screen.getByRole("heading", { name: "Circles" })).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByRole("heading", { name: "Circles" })).toBeInTheDocument();
});
expect(
screen.getByRole("heading", { name: "Elected Board" }),
).toBeInTheDocument();
@@ -105,24 +87,23 @@ describe("Page Flow Integration", () => {
screen.getByRole("heading", { name: "Petition" }),
).toBeInTheDocument();
// Feature Grid section
expect(
screen.getByRole("heading", {
name: "We've got your back, every step of the way",
}),
).toBeInTheDocument();
await waitFor(() => {
expect(
screen.getByRole("heading", {
name: "We've got your back, every step of the way",
}),
).toBeInTheDocument();
});
expect(
screen.getByText(
"Use our toolkit to improve, document, and evolve your organization.",
),
).toBeInTheDocument();
// Quote Block section
expect(
screen.getByText(/The rules of decision-making must be open/),
).toBeInTheDocument();
// Ask Organizer section
expect(
screen.getByRole("heading", { name: "Still have questions?" }),
).toBeInTheDocument();
@@ -136,27 +117,22 @@ describe("Page Flow Integration", () => {
test("hero banner CTA button is interactive", async () => {
const user = userEvent.setup();
render(<Page />);
renderPage();
// Get the first CTA button (multiple sizes for responsive design)
const ctaButtons = screen.getAllByRole("button", {
const ctaLinks = screen.getAllByRole("link", {
name: "Learn how CommunityRule works",
});
const ctaButton = ctaButtons[0];
expect(ctaButton).toBeInTheDocument();
// Button should be clickable (no href needed for button elements)
expect(ctaButton).toBeEnabled();
const ctaLink = ctaLinks[0];
expect(ctaLink).toBeInTheDocument();
expect(ctaLink).toHaveAttribute("href", "/how-it-works");
// Test button interaction
await user.click(ctaButton);
// Button should remain visible after click
expect(ctaButton).toBeInTheDocument();
await user.click(ctaLink);
expect(ctaLink).toBeInTheDocument();
});
test("CardSteps section shows step tiles with expected icon/color props", async () => {
render(<Page />);
renderPage();
// Wait for dynamically imported CardSteps component
await waitFor(() => {
const cards = screen.getAllByText(
/Document how your community|Build an operating manual|Get a link to your manual/,
@@ -164,19 +140,17 @@ describe("Page Flow Integration", () => {
expect(cards.length).toBeGreaterThan(0);
});
// Check that all three cards are rendered
const cards = screen.getAllByText(
/Document how your community|Build an operating manual|Get a link to your manual/,
);
expect(cards.length).toBeGreaterThan(0);
// Check that section numbers are present
const sectionNumbers = screen.getAllByText(/1|2|3/);
expect(sectionNumbers.length).toBeGreaterThan(0);
});
test("rule stack shows four featured templates and link to full catalog", async () => {
render(<Page />);
renderPage();
await waitFor(() => {
expect(screen.getByText("Circles")).toBeInTheDocument();
@@ -189,14 +163,14 @@ describe("Page Flow Integration", () => {
const seeAll = screen.getByRole("link", { name: "See all templates" });
expect(seeAll).toHaveAttribute("href", "/templates");
const seeHowButton = screen.getByRole("button", {
const seeHowLink = screen.getByRole("link", {
name: "See how it works",
});
expect(seeHowButton).toBeInTheDocument();
expect(seeHowLink).toHaveAttribute("href", "/how-it-works");
});
test("ask organizer section has proper call-to-action", () => {
render(<Page />);
renderPage();
const askCta = screen.getByRole("button", { name: /ask an organizer/i });
expect(askCta).toBeInTheDocument();
@@ -204,19 +178,16 @@ describe("Page Flow Integration", () => {
});
test("page maintains proper semantic structure", async () => {
render(<Page />);
renderPage();
// Wait for dynamically imported components to load
await waitFor(() => {
const headings = screen.getAllByRole("heading");
expect(headings.length).toBeGreaterThan(4); // Should have multiple headings
expect(headings.length).toBeGreaterThan(4);
});
// Check for proper heading hierarchy
const headings = screen.getAllByRole("heading");
expect(headings.length).toBeGreaterThan(4); // Should have multiple headings
expect(headings.length).toBeGreaterThan(4);
// Check that main content is properly structured
const mainContent = screen.getByText(
/Help your community make important decisions/,
);
@@ -224,7 +195,7 @@ describe("Page Flow Integration", () => {
});
test("all interactive elements are accessible", async () => {
render(<Page />);
renderPage();
await waitFor(() => {
expect(screen.getAllByRole("button").length).toBeGreaterThan(0);
@@ -245,33 +216,31 @@ describe("Page Flow Integration", () => {
});
});
// TODO: Fix next/dynamic mock to properly handle async component loading
test.skip("page content flows logically from top to bottom", async () => {
render(<Page />);
test("page content flows logically from top to bottom", async () => {
renderPage();
// Verify the logical flow of information
// 1. Hero introduces the concept
expect(
screen.getByText(/Help your community make important decisions/),
).toBeInTheDocument();
// 2. How it works section explains the process
expect(screen.getByText("How CommunityRule works")).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText("How CommunityRule works")).toBeInTheDocument();
});
// 3. Rule types show different governance options
expect(screen.getByText("Circles")).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText("Circles")).toBeInTheDocument();
});
// 4. Features highlight benefits
expect(
screen.getByText("We've got your back, every step of the way"),
).toBeInTheDocument();
await waitFor(() => {
expect(
screen.getByText("We've got your back, every step of the way"),
).toBeInTheDocument();
});
// 5. Quote provides social proof
expect(
screen.getByText(/The rules of decision-making must be open/),
).toBeInTheDocument();
// 6. Call to action for help
expect(screen.getByText("Still have questions?")).toBeInTheDocument();
});
});
+35 -22
View File
@@ -2,6 +2,7 @@ import {
renderWithProviders as render,
screen,
cleanup,
waitFor,
} from "../utils/test-utils";
import userEvent from "@testing-library/user-event";
import { describe, test, expect, afterEach, beforeEach, vi } from "vitest";
@@ -59,18 +60,24 @@ describe("Templates page (/templates)", () => {
<TemplatesPageClient initialGridEntries={GOVERNANCE_TEMPLATE_CATALOG} />,
);
const consensusCard = screen.getByText("Consensus").closest("div");
await user.click(consensusCard);
expect(testRouter.push).toHaveBeenCalledWith(
"/create/review-template/consensus",
await user.click(
screen.getByRole("button", { name: /Consensus/i }),
);
await waitFor(() => {
expect(testRouter.push).toHaveBeenCalledWith(
"/create/review-template/consensus",
);
});
testRouter.push.mockClear();
const solidarity = screen.getByText("Solidarity Network").closest("div");
await user.click(solidarity);
expect(testRouter.push).toHaveBeenCalledWith(
"/create/review-template/solidarity-network",
await user.click(
screen.getByRole("button", { name: /Solidarity Network/i }),
);
await waitFor(() => {
expect(testRouter.push).toHaveBeenCalledWith(
"/create/review-template/solidarity-network",
);
});
});
test("direct entry (no ?fromFlow=1): wipes anonymous draft before navigating", async () => {
@@ -80,16 +87,19 @@ describe("Templates page (/templates)", () => {
<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",
await user.click(
screen.getByRole("button", { name: /Consensus/i }),
);
await waitFor(() => {
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 () => {
@@ -102,8 +112,9 @@ describe("Templates page (/templates)", () => {
<TemplatesPageClient initialGridEntries={GOVERNANCE_TEMPLATE_CATALOG} />,
);
const consensusCard = screen.getByText("Consensus").closest("div");
await user.click(consensusCard);
await user.click(
screen.getByRole("button", { name: /Consensus/i }),
);
expect(window.localStorage.getItem(CREATE_FLOW_ANONYMOUS_KEY)).toBe(
JSON.stringify({ title: "Stale Community" }),
@@ -115,8 +126,10 @@ describe("Templates page (/templates)", () => {
);
// In-flow picks also pass `?fromFlow=1` on the template review URL so
// footer Back on `/create/review-template/…` returns to `/create/review`.
expect(testRouter.push).toHaveBeenCalledWith(
"/create/review-template/consensus?fromFlow=1",
);
await waitFor(() => {
expect(testRouter.push).toHaveBeenCalledWith(
"/create/review-template/consensus?fromFlow=1",
);
});
});
});
+42 -129
View File
@@ -8,67 +8,53 @@ import userEvent from "@testing-library/user-event";
import { vi, describe, test, expect, afterEach } from "vitest";
import React from "react";
import Page from "../../app/(marketing)/page";
// Mock next/dynamic so dynamically loaded components render after the import resolves
vi.mock("next/dynamic", () => {
const React = require("react");
return {
default: (importFn, options) => {
function DynamicWrapper(props) {
const [Component, setComponent] = React.useState(null);
React.useEffect(() => {
importFn().then((mod) => setComponent(() => mod.default || mod));
}, []);
if (!Component) {
return options?.loading ? options.loading() : null;
}
return <Component {...props} />;
}
return DynamicWrapper;
},
};
});
import Footer from "../../app/components/navigation/Footer";
vi.mock("next/dynamic", async () => {
const { default: syncDynamic } = await import("../utils/mockNextDynamicSync.js");
return { default: syncDynamic };
});
function renderPageWithFooter() {
return render(
<>
<Page />
<Footer />
</>,
);
}
function renderPage() {
return render(<Page />);
}
afterEach(() => {
cleanup();
});
describe("User Journey Integration", () => {
// TODO: Fix next/dynamic mock to properly handle async component loading
test.skip("new user discovers the application through hero section", async () => {
test("new user discovers the application through hero section", async () => {
const user = userEvent.setup();
render(
<div>
<Page />
<Footer />
</div>,
);
renderPageWithFooter();
// User sees the main value proposition
expect(
screen.getByText(/Help your community make important decisions/),
).toBeInTheDocument();
// User clicks the main CTA to learn more
const learnButtons = screen.getAllByRole("button", {
const learnLinks = screen.getAllByRole("link", {
name: "Learn how CommunityRule works",
});
const learnButton = learnButtons[0];
await user.click(learnButton);
await user.click(learnLinks[0]);
// Wait for dynamically imported CardSteps component
await waitFor(() => {
expect(screen.getByText("How CommunityRule works")).toBeInTheDocument();
});
});
// TODO: Fix next/dynamic mock to properly handle async component loading
test.skip("user explores different governance types", async () => {
test("user explores different governance types", async () => {
const user = userEvent.setup();
render(<Page />);
renderPage();
// Wait for dynamically imported RuleStack component
await waitFor(() => {
expect(screen.getByText("Circles")).toBeInTheDocument();
});
@@ -76,26 +62,18 @@ describe("User Journey Integration", () => {
expect(screen.getByText("Consensus")).toBeInTheDocument();
expect(screen.getByText("Petition")).toBeInTheDocument();
// User clicks on a governance type to create a rule
const seeHowButtons = screen.getAllByRole("button", {
const seeHowLinks = screen.getAllByRole("link", {
name: "See how it works",
});
expect(seeHowButtons.length).toBeGreaterThan(0);
expect(seeHowLinks.length).toBeGreaterThan(0);
await user.click(seeHowButtons[0]);
// Button should remain interactive
expect(seeHowButtons[0]).toBeInTheDocument();
await user.click(seeHowLinks[0]);
expect(seeHowLinks[0]).toBeInTheDocument();
});
test("user navigates through the application using header navigation", async () => {
render(
<div>
<Page />
<Footer />
</div>,
);
renderPageWithFooter();
// User clicks on navigation links in header (check that they exist and are clickable)
const navigationLinks = screen.getAllByRole("link");
const headerNavLinks = navigationLinks.filter(
(link) =>
@@ -104,7 +82,6 @@ describe("User Journey Integration", () => {
link.textContent?.includes("About"),
);
// Test that navigation links are present and clickable
for (const link of headerNavLinks) {
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute("href");
@@ -113,9 +90,8 @@ describe("User Journey Integration", () => {
test("user seeks help through ask organizer section", async () => {
const user = userEvent.setup();
render(<Page />);
renderPage();
// User scrolls to the bottom and sees the help section
expect(screen.getByText("Still have questions?")).toBeInTheDocument();
expect(
screen.getByText("Get answers from an experienced organizer"),
@@ -129,9 +105,8 @@ describe("User Journey Integration", () => {
});
test("user explores the process through CardSteps", async () => {
render(<Page />);
renderPage();
// Wait for dynamically imported CardSteps component
await waitFor(() => {
expect(
screen.getByText("Document how your community makes decisions"),
@@ -146,24 +121,16 @@ describe("User Journey Integration", () => {
),
).toBeInTheDocument();
// User sees the step numbers
const stepNumbers = screen.getAllByText(/1|2|3/);
expect(stepNumbers.length).toBeGreaterThan(0);
});
test("user accesses contact information through footer", async () => {
render(
<div>
<Page />
<Footer />
</div>,
);
renderPageWithFooter();
// User finds contact email in footer
const emailLink = screen.getByRole("link", { name: "medlab@colorado.edu" });
expect(emailLink).toHaveAttribute("href", "mailto:medlab@colorado.edu");
// User finds social media links
const blueskyLink = screen.getByRole("link", {
name: "Follow us on Bluesky",
});
@@ -176,9 +143,8 @@ describe("User Journey Integration", () => {
});
test("user explores features and benefits", async () => {
render(<Page />);
renderPage();
// Wait for dynamically imported FeatureGrid component
await waitFor(() => {
expect(
screen.getByText("We've got your back, every step of the way"),
@@ -190,21 +156,14 @@ describe("User Journey Integration", () => {
),
).toBeInTheDocument();
// User sees the testimonial/quote (check for the actual quote content)
expect(
screen.getByText(/The rules of decision-making must be open/),
).toBeInTheDocument();
});
test("user interacts with logo wall and social proof", async () => {
render(
<div>
<Page />
<Footer />
</div>,
);
renderPageWithFooter();
// Wait for dynamically imported LogoWall component
await waitFor(() => {
const logoImages = screen.getAllByRole("img");
const partnerLogos = logoImages.filter(
@@ -219,7 +178,6 @@ describe("User Journey Integration", () => {
expect(partnerLogos.length).toBeGreaterThan(0);
});
// Social links should be present in footer
const blueskyLink = screen.getByRole("link", { name: /Bluesky/i });
const gitlabLink = screen.getByRole("link", { name: /GitLab/i });
expect(blueskyLink).toBeInTheDocument();
@@ -227,76 +185,40 @@ describe("User Journey Integration", () => {
});
test("user completes the full journey from discovery to action", async () => {
render(
<div>
<Page />
<Footer />
</div>,
);
renderPageWithFooter();
// 1. User discovers the application
expect(screen.getByText("Collaborate")).toBeInTheDocument();
expect(screen.getByText("with clarity")).toBeInTheDocument();
// 2. User learns how it works - wait for dynamically imported component
await waitFor(() => {
expect(screen.getByText("How CommunityRule works")).toBeInTheDocument();
});
// 3. User sees governance options - wait for dynamically imported component
// Note: Dynamic imports may not resolve reliably in test environment
// Try to find governance content, but don't fail if dynamic import hasn't resolved
try {
await waitFor(
() => {
// Check for any of the governance card titles
const hasGovernanceContent =
screen.queryByText(/Circles/i) ||
screen.queryByText(/Elected Board/i) ||
screen.queryByText(/Petition/i);
expect(hasGovernanceContent).toBeTruthy();
},
{ timeout: 3000 },
);
} catch {
// Dynamic import may not resolve in test environment - this is a known limitation
// The component functionality is tested in RuleStack.test.jsx
console.warn(
"Dynamic import for RuleStack did not resolve in test environment",
);
}
await waitFor(() => {
expect(screen.getAllByText(/Circles/i).length).toBeGreaterThan(0);
});
// 4. User sees features and benefits - wait for dynamically imported component
await waitFor(() => {
expect(
screen.getByText("We've got your back, every step of the way"),
).toBeInTheDocument();
});
// 5. User sees social proof
expect(
screen.getByText(/The rules of decision-making must be open/),
).toBeInTheDocument();
// 6. User can take action
const seeHowButtons = screen.getAllByRole("button", {
const seeHowLinks = screen.getAllByRole("link", {
name: "See how it works",
});
expect(seeHowButtons.length).toBeGreaterThan(0);
expect(seeHowLinks.length).toBeGreaterThan(0);
// 7. User can get help if needed
expect(screen.getByText("Still have questions?")).toBeInTheDocument();
});
test("user can access all navigation options consistently", async () => {
render(
<div>
<Page />
<Footer />
</div>,
);
renderPageWithFooter();
// Footer navigation (header navigation is handled by layout, not in page component)
const footerLinks = screen.getAllByRole("link");
const navigationLinks = footerLinks.filter(
(link) =>
@@ -306,7 +228,6 @@ describe("User Journey Integration", () => {
);
expect(navigationLinks.length).toBeGreaterThan(0);
// All navigation links should be accessible
navigationLinks.forEach((link) => {
expect(link).not.toHaveAttribute("tabindex", "-1");
});
@@ -314,24 +235,16 @@ describe("User Journey Integration", () => {
test("user can complete actions without errors", async () => {
const user = userEvent.setup();
render(
<div>
<Page />
<Footer />
</div>,
);
renderPageWithFooter();
// Test all interactive elements
const buttons = screen.getAllByRole("button");
const links = screen.getAllByRole("link");
// All buttons should be clickable
for (const button of buttons) {
await user.click(button);
expect(button).toBeInTheDocument();
}
// All links should be accessible
for (const link of links) {
expect(link).toBeInTheDocument();
expect(link).not.toHaveAttribute("tabindex", "-1");