From c8d19089a753894dc39771031f9fb5ce42bcd443 Mon Sep 17 00:00:00 2001
From: adilallo <39313955+adilallo@users.noreply.github.com>
Date: Thu, 28 Aug 2025 21:34:37 -0600
Subject: [PATCH] Unit tests for main index components
---
tests/unit/AskOrganizer.test.jsx | 266 ++++++++++++++++++++++++++++++
tests/unit/FeatureGrid.test.jsx | 145 ++++++++++++++++
tests/unit/HeroBanner.test.jsx | 142 ++++++++++++++++
tests/unit/LogoWall.test.jsx | 166 +++++++++++++++++++
tests/unit/NumberedCards.test.jsx | 199 ++++++++++++++++++++++
tests/unit/QuoteBlock.test.jsx | 222 +++++++++++++++++++++++++
tests/unit/RuleStack.test.jsx | 200 ++++++++++++++++++++++
7 files changed, 1340 insertions(+)
create mode 100644 tests/unit/AskOrganizer.test.jsx
create mode 100644 tests/unit/FeatureGrid.test.jsx
create mode 100644 tests/unit/HeroBanner.test.jsx
create mode 100644 tests/unit/LogoWall.test.jsx
create mode 100644 tests/unit/NumberedCards.test.jsx
create mode 100644 tests/unit/QuoteBlock.test.jsx
create mode 100644 tests/unit/RuleStack.test.jsx
diff --git a/tests/unit/AskOrganizer.test.jsx b/tests/unit/AskOrganizer.test.jsx
new file mode 100644
index 0000000..278b6e1
--- /dev/null
+++ b/tests/unit/AskOrganizer.test.jsx
@@ -0,0 +1,266 @@
+import { render, screen, cleanup } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { vi, describe, test, expect, afterEach } from "vitest";
+import AskOrganizer from "../../app/components/AskOrganizer";
+
+afterEach(() => {
+ cleanup();
+});
+
+describe("AskOrganizer Component", () => {
+ test("renders with all props", () => {
+ render(
+
+ );
+
+ expect(
+ screen.getByRole("heading", { name: "Need help organizing?" })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("heading", { name: "Get expert guidance" })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText("Our organizers can help you build better communities")
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: "Contact an organizer" })
+ ).toBeInTheDocument();
+ });
+
+ test("renders with default button text", () => {
+ render();
+
+ expect(
+ screen.getByRole("button", { name: "Ask an organizer" })
+ ).toBeInTheDocument();
+ });
+
+ test("renders with custom className", () => {
+ render(
+
+ );
+
+ const section = document.querySelector("section");
+ expect(section).toHaveClass("custom-class");
+ });
+
+ test("renders different variants", () => {
+ const { rerender } = render(
+
+ );
+
+ // Centered variant should have center alignment
+ const container = screen
+ .getByRole("region")
+ .querySelector('[class*="text-center"]');
+ expect(container).toBeInTheDocument();
+
+ rerender(
+
+ );
+
+ // Left-aligned variant should have left alignment
+ const leftContainer = screen
+ .getByRole("region")
+ .querySelector('[class*="text-left"]');
+ expect(leftContainer).toBeInTheDocument();
+ });
+
+ test("renders ContentLockup with ask variant", () => {
+ render(
+
+ );
+
+ expect(
+ screen.getByRole("heading", { name: "Ask Title" })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("heading", { name: "Ask Subtitle" })
+ ).toBeInTheDocument();
+ expect(screen.getByText("Ask Description")).toBeInTheDocument();
+ });
+
+ test("renders button with correct props", () => {
+ render(
+
+ );
+
+ const button = screen.getByRole("button", { name: "Custom Button" });
+ expect(button).toHaveAttribute("href", "/custom");
+ expect(button).toHaveClass("size-large", "variant-default");
+ });
+
+ test("handles button click events", async () => {
+ const user = userEvent.setup();
+ const onContactClick = vi.fn();
+
+ render(
+
+ );
+
+ const button = screen.getByRole("button", { name: "Ask an organizer" });
+ await user.click(button);
+
+ expect(onContactClick).toHaveBeenCalledWith({
+ event: "contact_button_click",
+ component: "AskOrganizer",
+ variant: "centered",
+ buttonText: "Ask an organizer",
+ buttonHref: "#",
+ timestamp: expect.any(String),
+ });
+ });
+
+ test("applies analytics tracking", async () => {
+ const user = userEvent.setup();
+ const gtagSpy = vi.fn();
+
+ // Mock window.gtag
+ Object.defineProperty(window, "gtag", {
+ value: gtagSpy,
+ writable: true,
+ });
+
+ render();
+
+ const button = screen.getByRole("button", { name: "Ask an organizer" });
+ await user.click(button);
+
+ expect(gtagSpy).toHaveBeenCalledWith("event", "contact_button_click", {
+ event_category: "engagement",
+ event_label: "ask_organizer",
+ value: 1,
+ });
+ });
+
+ test("renders with proper accessibility attributes", () => {
+ render(
+
+ );
+
+ const section = document.querySelector("section");
+ expect(section).toHaveAttribute(
+ "aria-labelledby",
+ "ask-organizer-headline"
+ );
+ expect(section).toHaveAttribute("tabIndex", "-1");
+
+ const button = screen.getByRole("button", { name: "Custom Button" });
+ expect(button).toHaveAttribute(
+ "aria-label",
+ "Custom Button - Contact an organizer for help"
+ );
+ });
+
+ test("renders with design tokens", () => {
+ render();
+
+ const section = document.querySelector("section");
+ expect(section).toHaveClass(
+ "py-[var(--spacing-scale-032)]",
+ "px-[var(--spacing-scale-032)]"
+ );
+ });
+
+ test("applies responsive spacing", () => {
+ render();
+
+ const section = document.querySelector("section");
+ expect(section).toHaveClass(
+ "md:py-[var(--spacing-scale-096)]",
+ "md:px-[var(--spacing-scale-064)]"
+ );
+ });
+
+ test("renders with proper semantic structure", () => {
+ render();
+
+ const section = document.querySelector("section");
+ expect(section).toBeInTheDocument();
+
+ // Check for proper heading structure
+ const headings = screen.getAllByRole("heading");
+ expect(headings).toHaveLength(2); // title and subtitle
+ });
+
+ test("applies variant-specific styling", () => {
+ const { rerender } = render(
+
+ );
+
+ // Compact variant should have different padding
+ const section = screen.getByRole("region");
+ expect(section).toHaveClass(
+ "py-[var(--spacing-scale-016)]",
+ "px-[var(--spacing-scale-016)]"
+ );
+
+ rerender(
+
+ );
+
+ // Left-aligned variant should have left alignment
+ const container = section.querySelector('[class*="text-left"]');
+ expect(container).toBeInTheDocument();
+ });
+
+ test("renders button with custom styling", () => {
+ render();
+
+ const button = screen.getByRole("button", { name: "Ask an organizer" });
+ expect(button).toHaveClass(
+ "xl:!px-[var(--spacing-scale-020)]",
+ "xl:!py-[var(--spacing-scale-012)]"
+ );
+ });
+
+ test("handles missing optional props gracefully", () => {
+ render();
+
+ // Should still render the structure
+ const section = document.querySelector("section");
+ expect(section).toBeInTheDocument();
+
+ // Should render default button
+ expect(
+ screen.getByRole("button", { name: "Ask an organizer" })
+ ).toBeInTheDocument();
+ });
+
+ test("applies responsive button container alignment", () => {
+ render();
+
+ const buttonContainer = screen
+ .getByRole("button", { name: "Ask an organizer" })
+ .closest("div");
+ expect(buttonContainer).toHaveClass("flex", "justify-center");
+ });
+
+ test("renders with proper content gap", () => {
+ render();
+
+ const container = screen
+ .getByRole("region")
+ .querySelector('[class*="flex flex-col"]');
+ expect(container).toHaveClass("gap-[var(--spacing-scale-020)]");
+ });
+});
diff --git a/tests/unit/FeatureGrid.test.jsx b/tests/unit/FeatureGrid.test.jsx
new file mode 100644
index 0000000..47f69a7
--- /dev/null
+++ b/tests/unit/FeatureGrid.test.jsx
@@ -0,0 +1,145 @@
+import { render, screen, cleanup } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { vi, describe, test, expect, afterEach } from "vitest";
+import FeatureGrid from "../../app/components/FeatureGrid";
+
+afterEach(() => {
+ cleanup();
+});
+
+describe("FeatureGrid Component", () => {
+ test("renders with title and subtitle", () => {
+ render(
+
+ );
+
+ expect(
+ screen.getByRole("heading", { name: "Feature Tools" })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("heading", {
+ name: "Everything you need to build better communities",
+ })
+ ).toBeInTheDocument();
+ });
+
+ test("renders with custom className", () => {
+ render(
+
+ );
+
+ const section = document.querySelector("section");
+ expect(section).toHaveClass("custom-class");
+ });
+
+ test("renders all four MiniCard components", () => {
+ render();
+
+ // Check for all four MiniCard components
+ expect(
+ screen.getByRole("link", { name: "Decision-making support tools" })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("link", { name: "Values alignment exercises" })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("link", { name: "Membership guidance resources" })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("link", { name: "Conflict resolution tools" })
+ ).toBeInTheDocument();
+ });
+
+ test("renders ContentLockup with feature variant", () => {
+ render();
+
+ expect(
+ screen.getByRole("heading", { name: "Feature Title" })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("heading", { name: "Feature Subtitle" })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("link", { name: "Learn more" })
+ ).toBeInTheDocument();
+ });
+
+ test("has proper accessibility attributes", () => {
+ render();
+
+ const section = document.querySelector("section");
+ expect(section).toHaveAttribute("aria-labelledby", "feature-grid-headline");
+ expect(section).toHaveAttribute("tabIndex", "-1");
+
+ const grid = screen.getByRole("grid");
+ expect(grid).toHaveAttribute("aria-label", "Feature tools and services");
+ });
+
+ test("renders with design tokens", () => {
+ render();
+
+ const section = document.querySelector("section");
+ expect(section).toHaveClass("p-0", "lg:p-[var(--spacing-scale-064)]");
+
+ const container = section.querySelector('[class*="bg-[#171717]"]');
+ expect(container).toBeInTheDocument();
+ });
+
+ test("applies responsive grid layout", () => {
+ render();
+
+ const grid = screen.getByRole("grid");
+ expect(grid).toHaveClass("grid", "grid-cols-2", "md:grid-cols-4");
+ });
+
+ test("renders MiniCard with correct props", () => {
+ render();
+
+ // Check first MiniCard (Decision-making support)
+ const firstCard = screen.getByRole("link", {
+ name: "Decision-making support tools",
+ });
+ expect(firstCard).toHaveAttribute("href", "#decision-making");
+
+ // Check second MiniCard (Values alignment)
+ const secondCard = screen.getByRole("link", {
+ name: "Values alignment exercises",
+ });
+ expect(secondCard).toHaveAttribute("href", "#values-alignment");
+ });
+
+ test("renders with proper semantic structure", () => {
+ render();
+
+ const section = document.querySelector("section");
+ expect(section).toBeInTheDocument();
+
+ const grid = screen.getByRole("grid");
+ expect(grid).toBeInTheDocument();
+ });
+
+ test("handles missing optional props gracefully", () => {
+ render();
+
+ // Should still render the structure
+ const section = document.querySelector("section");
+ expect(section).toBeInTheDocument();
+
+ // Should render default MiniCards
+ expect(
+ screen.getByRole("link", { name: "Decision-making support tools" })
+ ).toBeInTheDocument();
+ });
+
+ test("applies focus-within styles", () => {
+ render();
+
+ const container = document
+ .querySelector("section")
+ .querySelector('[class*="focus-within:ring-2"]');
+ expect(container).toBeInTheDocument();
+ });
+});
diff --git a/tests/unit/HeroBanner.test.jsx b/tests/unit/HeroBanner.test.jsx
new file mode 100644
index 0000000..247e360
--- /dev/null
+++ b/tests/unit/HeroBanner.test.jsx
@@ -0,0 +1,142 @@
+import { render, screen, cleanup } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { vi, describe, test, expect, afterEach } from "vitest";
+import HeroBanner from "../../app/components/HeroBanner";
+
+afterEach(() => {
+ cleanup();
+});
+
+describe("HeroBanner Component", () => {
+ test("renders with all props", () => {
+ render(
+
+ );
+
+ expect(
+ screen.getByRole("heading", { name: "Welcome to CommunityRule" })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("heading", { name: "Build better communities" })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText("Create and manage community rules with ease")
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: "Get Started" })
+ ).toBeInTheDocument();
+ });
+
+ test("renders with minimal props", () => {
+ render();
+
+ expect(
+ screen.getByRole("heading", { name: "Minimal" })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("img", { name: "Hero illustration" })
+ ).toBeInTheDocument();
+ });
+
+ test("renders hero image", () => {
+ render();
+
+ const heroImage = screen.getByRole("img", { name: "Hero illustration" });
+ expect(heroImage).toBeInTheDocument();
+ expect(heroImage).toHaveAttribute("src", "assets/HeroImage.png");
+ });
+
+ test("applies correct CSS classes", () => {
+ render();
+
+ const section = document.querySelector("section");
+ expect(section).toHaveClass("bg-transparent");
+
+ const contentLockup = screen
+ .getByRole("heading", { name: "Test" })
+ .closest("div");
+ expect(contentLockup).toHaveClass("md:flex-1");
+ });
+
+ test("renders ContentLockup with correct props", () => {
+ render(
+
+ );
+
+ // Check that ContentLockup receives the props correctly
+ expect(
+ screen.getByRole("heading", { name: "Test Title" })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("heading", { name: "Test Subtitle" })
+ ).toBeInTheDocument();
+ expect(screen.getByText("Test Description")).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: "Test CTA" })
+ ).toBeInTheDocument();
+ });
+
+ test("renders HeroDecor component", () => {
+ render();
+
+ // HeroDecor should be present (it's a decorative component)
+ const heroDecor = document.querySelector(
+ '[class*="pointer-events-none absolute z-0"]'
+ );
+ expect(heroDecor).toBeInTheDocument();
+ });
+
+ test("has proper semantic structure", () => {
+ render();
+
+ const section = document.querySelector("section");
+ expect(section).toBeInTheDocument();
+
+ // Should have proper heading structure
+ const heading = screen.getByRole("heading", { name: "Test" });
+ expect(heading).toBeInTheDocument();
+ });
+
+ test("handles empty title gracefully", () => {
+ render();
+
+ // Should still render the structure even with empty title
+ const section = screen.getByRole("region");
+ expect(section).toBeInTheDocument();
+ });
+
+ test("applies responsive design classes", () => {
+ render();
+
+ const section = document.querySelector("section");
+ expect(section).toHaveClass(
+ "px-[var(--spacing-scale-008)]",
+ "sm:px-[var(--spacing-scale-010)]"
+ );
+ });
+
+ test("renders with design tokens", () => {
+ render();
+
+ const section = document.querySelector("section");
+ expect(section).toHaveClass("bg-transparent");
+
+ // Check for design token usage in the component structure
+ const container = section.querySelector(
+ '[class*="bg-[var(--color-surface-inverse-brand-primary)]"]'
+ );
+ expect(container).toBeInTheDocument();
+ });
+});
diff --git a/tests/unit/LogoWall.test.jsx b/tests/unit/LogoWall.test.jsx
new file mode 100644
index 0000000..6763fd6
--- /dev/null
+++ b/tests/unit/LogoWall.test.jsx
@@ -0,0 +1,166 @@
+import { render, screen, cleanup } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { vi, describe, test, expect, afterEach } from "vitest";
+import LogoWall from "../../app/components/LogoWall";
+
+afterEach(() => {
+ cleanup();
+});
+
+describe("LogoWall Component", () => {
+ test("renders with default logos", () => {
+ render();
+
+ // Check for default logos
+ expect(screen.getByAltText("Food Not Bombs")).toBeInTheDocument();
+ 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();
+ });
+
+ test("renders with custom logos", () => {
+ const customLogos = [
+ {
+ src: "assets/custom1.png",
+ alt: "Custom Logo 1",
+ size: "h-8",
+ order: "order-1",
+ },
+ {
+ src: "assets/custom2.png",
+ alt: "Custom Logo 2",
+ size: "h-10",
+ order: "order-2",
+ },
+ ];
+
+ render();
+
+ expect(screen.getByAltText("Custom Logo 1")).toBeInTheDocument();
+ expect(screen.getByAltText("Custom Logo 2")).toBeInTheDocument();
+ expect(screen.queryByAltText("Food Not Bombs")).not.toBeInTheDocument();
+ });
+
+ test("renders section label", () => {
+ render();
+
+ expect(
+ screen.getByText("Trusted by leading cooperators")
+ ).toBeInTheDocument();
+ });
+
+ test("applies correct CSS classes", () => {
+ render();
+
+ const section = document.querySelector("section");
+ expect(section).toHaveClass("p-[var(--spacing-scale-032)]");
+ });
+
+ test("renders with design tokens", () => {
+ render();
+
+ const section = document.querySelector("section");
+ expect(section).toHaveClass(
+ "p-[var(--spacing-scale-032)]",
+ "md:px-[var(--spacing-scale-024)]"
+ );
+ });
+
+ test("applies responsive grid layout", () => {
+ render();
+
+ const grid = document.querySelector(
+ '[class*="grid grid-cols-2 grid-rows-3"]'
+ );
+ expect(grid).toBeInTheDocument();
+ expect(grid).toHaveClass("sm:grid-cols-3", "sm:grid-rows-2", "md:flex");
+ });
+
+ test("renders logos with correct attributes", () => {
+ render();
+
+ const foodNotBombsLogo = screen.getByAltText("Food Not Bombs");
+ expect(foodNotBombsLogo).toHaveAttribute(
+ "src",
+ "assets/Section/Logo_FoodNotBombs.png"
+ );
+ expect(foodNotBombsLogo).toHaveClass("h-11", "lg:h-14", "xl:h-[70px]");
+ });
+
+ test("applies order classes for responsive layout", () => {
+ render();
+
+ const foodNotBombsContainer = screen
+ .getByAltText("Food Not Bombs")
+ .closest("div");
+ expect(foodNotBombsContainer).toHaveClass("order-1", "sm:order-4");
+ });
+
+ test("handles empty logos array", () => {
+ render();
+
+ // Should fall back to default logos
+ expect(screen.getByAltText("Food Not Bombs")).toBeInTheDocument();
+ });
+
+ test("applies hover effects", () => {
+ render();
+
+ const logoContainers = document.querySelectorAll(
+ '[class*="hover:opacity-100"]'
+ );
+ expect(logoContainers.length).toBeGreaterThan(0);
+ });
+
+ test("renders with proper semantic structure", () => {
+ render();
+
+ const section = document.querySelector("section");
+ expect(section).toBeInTheDocument();
+
+ // Check for the label
+ const label = screen.getByText("Trusted by leading cooperators");
+ expect(label).toBeInTheDocument();
+ });
+
+ test("applies transition effects", () => {
+ render();
+
+ const logoContainers = document.querySelectorAll(
+ '[class*="transition-opacity duration-500"]'
+ );
+ expect(logoContainers.length).toBeGreaterThan(0);
+ });
+
+ test("renders with proper image optimization", () => {
+ render();
+
+ const logos = screen.getAllByRole("img");
+ logos.forEach((logo) => {
+ expect(logo).toHaveAttribute("unoptimized");
+ expect(logo).toHaveAttribute("sizes", "100vw");
+ });
+ });
+
+ test("prioritizes first two logos", () => {
+ render();
+
+ const logos = screen.getAllByRole("img");
+ const foodNotBombsLogo = logos.find((img) => img.alt === "Food Not Bombs");
+ const startCoopLogo = logos.find((img) => img.alt === "Start COOP");
+
+ expect(foodNotBombsLogo).toHaveAttribute("priority");
+ expect(startCoopLogo).toHaveAttribute("priority");
+ });
+
+ test("applies scale effect on hover", () => {
+ render();
+
+ const logos = screen.getAllByRole("img");
+ logos.forEach((logo) => {
+ expect(logo).toHaveClass("hover:scale-105");
+ });
+ });
+});
diff --git a/tests/unit/NumberedCards.test.jsx b/tests/unit/NumberedCards.test.jsx
new file mode 100644
index 0000000..309a2d4
--- /dev/null
+++ b/tests/unit/NumberedCards.test.jsx
@@ -0,0 +1,199 @@
+import { render, screen, cleanup } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { vi, describe, test, expect, afterEach } from "vitest";
+import NumberedCards from "../../app/components/NumberedCards";
+
+afterEach(() => {
+ cleanup();
+});
+
+describe("NumberedCards Component", () => {
+ const mockCards = [
+ {
+ text: "Define your community values",
+ iconShape: "circle",
+ iconColor: "blue",
+ },
+ {
+ text: "Create decision-making processes",
+ iconShape: "square",
+ iconColor: "green",
+ },
+ {
+ text: "Establish communication channels",
+ iconShape: "triangle",
+ iconColor: "red",
+ },
+ ];
+
+ test("renders with title, subtitle, and cards", () => {
+ render(
+
+ );
+
+ expect(
+ screen.getByRole("heading", { name: "How CommunityRule helps" })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("heading", {
+ name: "Build better communities step by step",
+ })
+ ).toBeInTheDocument();
+
+ // Check for card content
+ expect(
+ screen.getByText("Define your community values")
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText("Create decision-making processes")
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText("Establish communication channels")
+ ).toBeInTheDocument();
+ });
+
+ test("renders SectionHeader component", () => {
+ render(
+
+ );
+
+ expect(
+ screen.getByRole("heading", { name: "Test Title" })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("heading", { name: "Test Subtitle" })
+ ).toBeInTheDocument();
+ });
+
+ test("renders NumberedCard components with correct props", () => {
+ render();
+
+ // Check that NumberedCard components receive correct props
+ expect(screen.getByText("1")).toBeInTheDocument(); // First card number
+ expect(screen.getByText("2")).toBeInTheDocument(); // Second card number
+ expect(screen.getByText("3")).toBeInTheDocument(); // Third card number
+ });
+
+ test("renders call-to-action buttons", () => {
+ render();
+
+ expect(
+ screen.getByRole("button", { name: "Create CommunityRule" })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: "See how it works" })
+ ).toBeInTheDocument();
+ });
+
+ test("applies responsive button visibility", () => {
+ render();
+
+ const createButton = screen.getByRole("button", {
+ name: "Create CommunityRule",
+ });
+ const seeHowButton = screen.getByRole("button", {
+ name: "See how it works",
+ });
+
+ expect(createButton.closest("div")).toHaveClass("block", "lg:hidden");
+ expect(seeHowButton.closest("div")).toHaveClass("hidden", "lg:block");
+ });
+
+ test("renders with design tokens", () => {
+ render();
+
+ const section = document.querySelector("section");
+ expect(section).toHaveClass(
+ "bg-transparent",
+ "py-[var(--spacing-scale-032)]"
+ );
+ });
+
+ test("applies responsive grid layout", () => {
+ render();
+
+ const cardsContainer = document.querySelector(
+ '[class*="grid grid-cols-1"]'
+ );
+ expect(cardsContainer).toBeInTheDocument();
+ });
+
+ test("renders schema markup", () => {
+ render(
+
+ );
+
+ const script = document.querySelector('script[type="application/ld+json"]');
+ expect(script).toBeInTheDocument();
+
+ const schemaData = JSON.parse(script.textContent);
+ expect(schemaData["@type"]).toBe("HowTo");
+ expect(schemaData.name).toBe("Test Title");
+ expect(schemaData.description).toBe("Test Description");
+ expect(schemaData.step).toHaveLength(3);
+ });
+
+ test("has proper semantic structure", () => {
+ render();
+
+ const section = document.querySelector("section");
+ expect(section).toBeInTheDocument();
+
+ // Check for proper heading structure
+ const headings = screen.getAllByRole("heading");
+ expect(headings.length).toBeGreaterThan(0);
+ });
+
+ test("handles empty cards array", () => {
+ render();
+
+ // Should still render the structure
+ const section = document.querySelector("section");
+ expect(section).toBeInTheDocument();
+
+ // Should render buttons even without cards
+ expect(
+ screen.getByRole("button", { name: "Create CommunityRule" })
+ ).toBeInTheDocument();
+ });
+
+ test("applies responsive text alignment", () => {
+ render();
+
+ const buttonContainer = screen
+ .getByRole("button", { name: "Create CommunityRule" })
+ .closest("div");
+ expect(buttonContainer).toHaveClass("block", "lg:hidden");
+ });
+
+ test("renders with proper spacing", () => {
+ render();
+
+ const section = document.querySelector("section");
+ expect(section).toHaveClass(
+ "py-[var(--spacing-scale-032)]",
+ "sm:py-[var(--spacing-scale-048)]"
+ );
+ });
+
+ test("applies max-width constraint", () => {
+ render();
+
+ const container = document.querySelector(
+ '[class*="max-w-[var(--spacing-measures-max-width-lg)]"]'
+ );
+ expect(container).toBeInTheDocument();
+ });
+});
diff --git a/tests/unit/QuoteBlock.test.jsx b/tests/unit/QuoteBlock.test.jsx
new file mode 100644
index 0000000..40ffca3
--- /dev/null
+++ b/tests/unit/QuoteBlock.test.jsx
@@ -0,0 +1,222 @@
+import { render, screen, cleanup } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { vi, describe, test, expect, afterEach } from "vitest";
+import QuoteBlock from "../../app/components/QuoteBlock";
+
+afterEach(() => {
+ cleanup();
+});
+
+describe("QuoteBlock Component", () => {
+ test("renders with default props", () => {
+ render();
+
+ expect(
+ screen.getByText(/The rules of decision-making must be open/)
+ ).toBeInTheDocument();
+ expect(screen.getByText("Jo Freeman")).toBeInTheDocument();
+ expect(
+ screen.getByText("The Tyranny of Structurelessness")
+ ).toBeInTheDocument();
+ expect(screen.getByAltText("Portrait of Jo Freeman")).toBeInTheDocument();
+ });
+
+ test("renders with custom props", () => {
+ render(
+
+ );
+
+ expect(screen.getByText("Custom quote text")).toBeInTheDocument();
+ expect(screen.getByText("Custom Author")).toBeInTheDocument();
+ expect(screen.getByText("Custom Source")).toBeInTheDocument();
+ });
+
+ test("renders with custom className", () => {
+ render(
+
+ );
+
+ const section = document.querySelector("section");
+ expect(section).toHaveClass("custom-class");
+ });
+
+ test("renders different variants", () => {
+ const { rerender } = render(
+
+ );
+
+ // Compact variant should have different styling
+ const section = screen.getByRole("region");
+ expect(section).toHaveClass(
+ "py-[var(--spacing-scale-032)]",
+ "px-[var(--spacing-scale-016)]"
+ );
+
+ rerender(
+
+ );
+
+ // Extended variant should have different styling
+ expect(section).toHaveClass(
+ "py-[var(--spacing-scale-048)]",
+ "px-[var(--spacing-scale-024)]"
+ );
+ });
+
+ test("renders with custom ID", () => {
+ render(
+
+ );
+
+ const quoteElement = screen.getByText("Test quote");
+ expect(quoteElement).toBeInTheDocument();
+ });
+
+ test("handles image error gracefully", () => {
+ render();
+
+ // Should render the quote and author
+ expect(screen.getByText("Test quote")).toBeInTheDocument();
+ expect(screen.getByText("Test Author")).toBeInTheDocument();
+ });
+
+ test("calls onError callback when image fails", () => {
+ const onError = vi.fn();
+ render(
+
+ );
+
+ // Should render without errors
+ expect(screen.getByText("Test quote")).toBeInTheDocument();
+ });
+
+ test("renders with fallback avatar", () => {
+ render();
+
+ // Should render the quote and author
+ expect(screen.getByText("Test quote")).toBeInTheDocument();
+ expect(screen.getByText("Test Author")).toBeInTheDocument();
+ });
+
+ test("renders decorative elements for standard variant", () => {
+ render(
+
+ );
+
+ // Should render QuoteDecor for standard variant
+ const decor = document.querySelector(
+ '[class*="pointer-events-none absolute z-0"]'
+ );
+ expect(decor).toBeInTheDocument();
+ });
+
+ test("does not render decorative elements for compact variant", () => {
+ render(
+
+ );
+
+ // Should not render QuoteDecor for compact variant
+ const decor = document.querySelector(
+ '[class*="pointer-events-none absolute z-0"]'
+ );
+ expect(decor).not.toBeInTheDocument();
+ });
+
+ test("renders with proper semantic structure", () => {
+ render();
+
+ const section = document.querySelector("section");
+ expect(section).toBeInTheDocument();
+
+ const blockquote = document.querySelector("blockquote");
+ expect(blockquote).toBeInTheDocument();
+
+ const cite = document.querySelector("cite");
+ expect(cite).toBeInTheDocument();
+ });
+
+ test("applies correct accessibility attributes", () => {
+ render(
+
+ );
+
+ const section = document.querySelector("section");
+ expect(section).toHaveAttribute("aria-labelledby", "test-quote-content");
+
+ const blockquote = document.querySelector("blockquote");
+ expect(blockquote).toHaveAttribute("aria-labelledby", "test-quote-author");
+ });
+
+ test("renders with design tokens", () => {
+ render();
+
+ const section = document.querySelector("section");
+ expect(section).toHaveClass("md:py-[var(--spacing-scale-032)]");
+
+ const card = section.querySelector(
+ '[class*="bg-[var(--color-surface-default-brand-darker-accent)]"]'
+ );
+ expect(card).toBeInTheDocument();
+ });
+
+ test("handles missing required props", () => {
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
+
+ render();
+
+ expect(consoleSpy).toHaveBeenCalledWith(
+ "QuoteBlock: Missing required props (quote or author)"
+ );
+
+ consoleSpy.mockRestore();
+ });
+
+ test("renders with proper quote styling", () => {
+ render();
+
+ const quoteElement = screen.getByText("Test quote");
+ expect(quoteElement).toHaveClass("font-bricolage-grotesque", "font-normal");
+ });
+
+ test("renders with proper author styling", () => {
+ render();
+
+ const authorElement = screen.getByText("Test Author");
+ expect(authorElement).toHaveClass("font-inter", "font-normal", "uppercase");
+ });
+
+ test("applies responsive text sizing", () => {
+ render(
+
+ );
+
+ const quoteElement = screen.getByText("Test quote");
+ expect(quoteElement).toHaveClass(
+ "text-[18px]",
+ "md:text-[36px]",
+ "lg:text-[52px]"
+ );
+ });
+
+ test("renders without source when not provided", () => {
+ render();
+
+ expect(screen.getByText("Test quote")).toBeInTheDocument();
+ expect(screen.getByText("Test Author")).toBeInTheDocument();
+ expect(
+ screen.queryByText("The Tyranny of Structurelessness")
+ ).not.toBeInTheDocument();
+ });
+});
diff --git a/tests/unit/RuleStack.test.jsx b/tests/unit/RuleStack.test.jsx
new file mode 100644
index 0000000..6fa383a
--- /dev/null
+++ b/tests/unit/RuleStack.test.jsx
@@ -0,0 +1,200 @@
+import { render, screen, cleanup } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { vi, describe, test, expect, afterEach } from "vitest";
+import RuleStack from "../../app/components/RuleStack";
+
+afterEach(() => {
+ cleanup();
+});
+
+describe("RuleStack Component", () => {
+ test("renders all four rule cards", () => {
+ render();
+
+ expect(screen.getByText("Consensus clusters")).toBeInTheDocument();
+ expect(screen.getByText("Consensus")).toBeInTheDocument();
+ expect(screen.getByText("Elected Board")).toBeInTheDocument();
+ expect(screen.getByText("Petition")).toBeInTheDocument();
+ });
+
+ test("renders with custom className", () => {
+ render();
+
+ const section = document.querySelector("section");
+ expect(section).toHaveClass("custom-class");
+ });
+
+ test("renders rule card descriptions", () => {
+ render();
+
+ expect(
+ screen.getByText(/Units called Circles have the ability to decide/)
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(/Decisions that affect the group collectively/)
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(/An elected board determines policies/)
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(/All participants can propose and vote/)
+ ).toBeInTheDocument();
+ });
+
+ test("renders rule card icons", () => {
+ render();
+
+ expect(screen.getByAltText("Sociocracy")).toBeInTheDocument();
+ expect(screen.getByAltText("Consensus")).toBeInTheDocument();
+ expect(screen.getByAltText("Elected Board")).toBeInTheDocument();
+ expect(screen.getByAltText("Petition")).toBeInTheDocument();
+ });
+
+ test("renders call-to-action button", () => {
+ render();
+
+ expect(
+ screen.getByRole("button", { name: "See all templates" })
+ ).toBeInTheDocument();
+ });
+
+ test("applies correct CSS classes", () => {
+ render();
+
+ const section = document.querySelector("section");
+ expect(section).toHaveClass("w-full", "bg-transparent");
+ });
+
+ test("renders with design tokens", () => {
+ render();
+
+ const section = document.querySelector("section");
+ expect(section).toHaveClass(
+ "py-[var(--spacing-scale-032)]",
+ "px-[var(--spacing-scale-020)]"
+ );
+ });
+
+ test("applies responsive grid layout", () => {
+ render();
+
+ const grid = document.querySelector('[class*="flex flex-col gap-[18px]"]');
+ expect(grid).toHaveClass("xmd:grid", "xmd:grid-cols-2");
+ });
+
+ test("renders RuleCard components with correct props", () => {
+ render();
+
+ // Check that RuleCard components receive correct props
+ const consensusClustersCard = screen
+ .getByText("Consensus clusters")
+ .closest('[class*="bg-[var(--color-surface-default-brand-lime)]"]');
+ expect(consensusClustersCard).toBeInTheDocument();
+
+ const consensusCard = screen
+ .getByText("Consensus")
+ .closest('[class*="bg-[var(--color-surface-default-brand-rust)]"]');
+ expect(consensusCard).toBeInTheDocument();
+ });
+
+ test("handles template click events", async () => {
+ const user = userEvent.setup();
+ const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
+
+ render();
+
+ const consensusCard = screen.getByText("Consensus").closest("div");
+ await user.click(consensusCard);
+
+ expect(consoleSpy).toHaveBeenCalledWith("Consensus template clicked");
+
+ consoleSpy.mockRestore();
+ });
+
+ test("renders with proper semantic structure", () => {
+ render();
+
+ const section = document.querySelector("section");
+ expect(section).toBeInTheDocument();
+
+ // Check for proper heading structure in cards
+ const headings = screen.getAllByRole("heading");
+ expect(headings).toHaveLength(4); // Four rule cards
+ });
+
+ test("applies responsive spacing", () => {
+ render();
+
+ const section = document.querySelector("section");
+ expect(section).toHaveClass(
+ "md:py-[var(--spacing-scale-048)]",
+ "lg:py-[var(--spacing-scale-064)]"
+ );
+ });
+
+ test("renders icons with correct attributes", () => {
+ render();
+
+ const sociocracyIcon = screen.getByAltText("Sociocracy");
+ expect(sociocracyIcon).toHaveAttribute("src", "assets/Icon_Sociocracy.svg");
+ expect(sociocracyIcon).toHaveClass(
+ "md:w-[56px]",
+ "md:h-[56px]",
+ "lg:w-[90px]",
+ "lg:h-[90px]"
+ );
+ });
+
+ test("applies different background colors to cards", () => {
+ render();
+
+ const cards = document.querySelectorAll(
+ '[class*="bg-[var(--color-surface-default-brand-"]'
+ );
+ expect(cards.length).toBeGreaterThan(0);
+ });
+
+ test("renders with proper button styling", () => {
+ render();
+
+ const button = screen.getByRole("button", { name: "See all templates" });
+ expect(button).toHaveClass("bg-transparent", "border-[1.5px]");
+ });
+
+ test("applies flex layout for button container", () => {
+ render();
+
+ const buttonContainer = screen
+ .getByRole("button", { name: "See all templates" })
+ .closest("div");
+ expect(buttonContainer).toHaveClass("flex", "justify-center");
+ });
+
+ test("handles analytics tracking", async () => {
+ const user = userEvent.setup();
+ const gtagSpy = vi.fn();
+ const analyticsSpy = vi.fn();
+
+ // Mock window.gtag and window.analytics
+ Object.defineProperty(window, "gtag", {
+ value: gtagSpy,
+ writable: true,
+ });
+ Object.defineProperty(window, "analytics", {
+ value: { track: analyticsSpy },
+ writable: true,
+ });
+
+ render();
+
+ const electedBoardCard = screen.getByText("Elected Board").closest("div");
+ await user.click(electedBoardCard);
+
+ expect(gtagSpy).toHaveBeenCalledWith("event", "template_click", {
+ template_name: "Elected Board",
+ });
+ expect(analyticsSpy).toHaveBeenCalledWith("Template Clicked", {
+ templateName: "Elected Board",
+ });
+ });
+});