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