Right rail template

This commit is contained in:
adilallo
2026-02-28 23:16:10 -07:00
parent f5bfb25f5e
commit 0799636c78
60 changed files with 1255 additions and 305 deletions
+9 -1
View File
@@ -5,7 +5,15 @@ import ContentBanner from "../../app/components/sections/ContentBanner";
import type { BlogPost } from "../../lib/content";
vi.mock("next/link", () => ({
default: ({ children, href, ...props }: any) => (
default: ({
children,
href,
...props
}: {
children?: React.ReactNode;
href?: string;
[key: string]: unknown;
}) => (
<a href={href} {...props}>
{children}
</a>
+3 -1
View File
@@ -44,7 +44,9 @@ describe("CreateFlowFooter (behavioral tests)", () => {
it("renders progress bar when progressBar is true", () => {
render(<CreateFlowFooter progressBar={true} />);
const footer = screen.getByRole("contentinfo", { name: "Create Flow Footer" });
const footer = screen.getByRole("contentinfo", {
name: "Create Flow Footer",
});
expect(footer).toBeInTheDocument();
});
+3 -1
View File
@@ -80,7 +80,9 @@ describe("CreateFlowTopNav (behavioral tests)", () => {
it("does not render Share button when hasShare is false", () => {
render(<CreateFlowTopNav hasShare={false} />);
expect(screen.queryByRole("button", { name: "Share" })).not.toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Share" }),
).not.toBeInTheDocument();
});
it("renders Export button when hasExport is true", () => {
+2 -10
View File
@@ -45,9 +45,7 @@ describe("HeaderLockup (behavioral tests)", () => {
});
it("renders description when provided", () => {
render(
<HeaderLockup title="Test Title" description="Test description" />,
);
render(<HeaderLockup title="Test Title" description="Test description" />);
expect(screen.getByText("Test description")).toBeInTheDocument();
});
@@ -71,13 +69,7 @@ describe("HeaderLockup (behavioral tests)", () => {
});
it("accepts PascalCase props", () => {
render(
<HeaderLockup
title="Test Title"
justification="Left"
size="L"
/>,
);
render(<HeaderLockup title="Test Title" justification="Left" size="L" />);
expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument();
});
});
+1 -1
View File
@@ -47,7 +47,7 @@ describe("Logo (behavioral tests)", () => {
it("hides text when showText is false", () => {
const { container } = render(<Logo showText={false} />);
const textElement = container.querySelector('.hidden');
const textElement = container.querySelector(".hidden");
expect(textElement).toBeInTheDocument();
expect(screen.getByAltText("CommunityRule Logo Icon")).toBeInTheDocument();
});
+6 -12
View File
@@ -135,13 +135,10 @@ describe("MultiSelect behaviour specifics", () => {
});
it("does not render add button when addButton is false", () => {
render(
<MultiSelect
options={defaultChipOptions}
addButton={false}
/>,
);
expect(screen.queryByRole("button", { name: /add/i })).not.toBeInTheDocument();
render(<MultiSelect options={defaultChipOptions} addButton={false} />);
expect(
screen.queryByRole("button", { name: /add/i }),
).not.toBeInTheDocument();
});
it("handles custom chip confirm", async () => {
@@ -163,7 +160,7 @@ describe("MultiSelect behaviour specifics", () => {
// Now the check button should be enabled
const checkButton = screen.getByRole("button", { name: "Confirm" });
expect(checkButton).not.toBeDisabled();
await userEvent.click(checkButton);
expect(handleConfirm).toHaveBeenCalledWith("custom-1", "NewOption");
@@ -175,10 +172,7 @@ describe("MultiSelect behaviour specifics", () => {
{ id: "custom-1", label: "", state: "Custom" as const },
];
render(
<MultiSelect
options={customOptions}
onCustomChipClose={handleClose}
/>,
<MultiSelect options={customOptions} onCustomChipClose={handleClose} />,
);
const closeButton = screen.getByRole("button", { name: "Close" });
+9 -1
View File
@@ -5,7 +5,15 @@ import RelatedArticles from "../../app/components/sections/RelatedArticles";
import type { BlogPost } from "../../lib/content";
vi.mock("next/link", () => ({
default: ({ children, href, ...props }: any) => (
default: ({
children,
href,
...props
}: {
children?: React.ReactNode;
href?: string;
[key: string]: unknown;
}) => (
<a href={href} {...props}>
{children}
</a>
+6 -2
View File
@@ -13,7 +13,9 @@ describe("ReviewPage", () => {
it("renders HeaderLockup with expected title", () => {
render(<ReviewPage />);
expect(
screen.getByRole("heading", { name: "Your community is added - congrats!" }),
screen.getByRole("heading", {
name: "Your community is added - congrats!",
}),
).toBeInTheDocument();
});
@@ -44,6 +46,8 @@ describe("ReviewPage", () => {
render(<ReviewPage />);
const buttons = screen.getAllByRole("button");
expect(buttons.length).toBeGreaterThanOrEqual(1);
expect(buttons.some((el) => el.textContent?.includes("Mutual Aid Mondays"))).toBe(true);
expect(
buttons.some((el) => el.textContent?.includes("Mutual Aid Mondays")),
).toBe(true);
});
});
+2 -4
View File
@@ -1,6 +1,6 @@
import React from "react";
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { render } from "@testing-library/react";
import "@testing-library/jest-dom/vitest";
import TextInput from "../../app/components/controls/TextInput";
import { componentTestSuite } from "../utils/componentTestSuite";
@@ -34,9 +34,7 @@ componentTestSuite<TextInputProps>({
describe("TextInput (size tests)", () => {
it("renders with medium size by default", () => {
const { container } = render(
<TextInput label="Test" inputSize="medium" />,
);
const { container } = render(<TextInput label="Test" inputSize="medium" />);
const input = container.querySelector("input");
expect(input).toHaveClass("h-[40px]");
});
+21 -7
View File
@@ -33,13 +33,17 @@ describe("Upload (behavioral tests)", () => {
it("renders with active state by default", () => {
render(<Upload label="Upload" />);
const button = screen.getByRole("button", { name: /upload/i });
expect(button).toHaveClass("bg-[var(--color-surface-invert-primary,white)]");
expect(button).toHaveClass(
"bg-[var(--color-surface-invert-primary,white)]",
);
});
it("renders with inactive state when active is false", () => {
render(<Upload label="Upload" active={false} />);
const button = screen.getByRole("button", { name: /upload/i });
expect(button).toHaveClass("bg-[var(--color-surface-default-secondary,#141414)]");
expect(button).toHaveClass(
"bg-[var(--color-surface-default-secondary,#141414)]",
);
});
it("displays label when provided", () => {
@@ -76,20 +80,30 @@ describe("Upload (behavioral tests)", () => {
it("displays description text", () => {
render(<Upload label="Upload" />);
expect(screen.getByText(/Add images, PDFs, and other files to the policy/i)).toBeInTheDocument();
expect(
screen.getByText(/Add images, PDFs, and other files to the policy/i),
).toBeInTheDocument();
});
it("applies active state styles correctly", () => {
render(<Upload label="Upload" active={true} />);
const descriptionText = screen.getByText(/Add images, PDFs, and other files to the policy/i);
const descriptionText = screen.getByText(
/Add images, PDFs, and other files to the policy/i,
);
const descriptionContainer = descriptionText.parentElement;
expect(descriptionContainer).toHaveClass("text-[color:var(--color-content-default-primary,white)]");
expect(descriptionContainer).toHaveClass(
"text-[color:var(--color-content-default-primary,white)]",
);
});
it("applies inactive state styles correctly", () => {
render(<Upload label="Upload" active={false} />);
const descriptionText = screen.getByText(/Add images, PDFs, and other files to the policy/i);
const descriptionText = screen.getByText(
/Add images, PDFs, and other files to the policy/i,
);
const descriptionContainer = descriptionText.parentElement;
expect(descriptionContainer).toHaveClass("text-[color:var(--color-content-default-tertiary,#b4b4b4)]");
expect(descriptionContainer).toHaveClass(
"text-[color:var(--color-content-default-tertiary,#b4b4b4)]",
);
});
});
+5 -1
View File
@@ -74,7 +74,11 @@ test.describe("Performance Monitoring", () => {
const metrics: { lcp?: number; fid?: number; cls?: number } = {};
for (const entry of entries) {
const e = entry as any;
const e = entry as PerformanceEntry & {
startTime?: number;
processingStart?: number;
value?: number;
};
if (
e.name === "LCP" ||
e.entryType === "largest-contentful-paint"
+2 -2
View File
@@ -1,7 +1,7 @@
import { test, expect } from "@playwright/test";
import { test, expect, type Page } from "@playwright/test";
test.describe("Visual Regression Tests", () => {
async function settle(page: any) {
async function settle(page: Page) {
await page.evaluate(() => {
window.scrollTo(0, window.scrollY); // ensure a frame boundary
void document.body.getBoundingClientRect();
+6 -2
View File
@@ -32,7 +32,9 @@ describe("Create flow cards page", () => {
render(<CardsPage />);
expect(
screen.getByText("How should this community communicate with each-other?"),
screen.getByText(
"How should this community communicate with each-other?",
),
).toBeInTheDocument();
});
@@ -58,7 +60,9 @@ describe("Create flow cards page", () => {
});
await user.click(toggle);
expect(screen.getByRole("button", { name: "Show less" })).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Show less" }),
).toBeInTheDocument();
expect(
screen.getByText(
"What method should this community use to communicate with eachother?",
+145
View File
@@ -0,0 +1,145 @@
import {
renderWithProviders as render,
screen,
cleanup,
within,
} from "../utils/test-utils";
import userEvent from "@testing-library/user-event";
import { describe, test, expect, afterEach } from "vitest";
import RightRailPage from "../../app/create/right-rail/page";
afterEach(() => {
cleanup();
});
describe("Create flow right-rail page", () => {
test("renders without error", () => {
render(<RightRailPage />);
expect(
screen.getByRole("heading", {
name: "How should conflicts be resolved?",
}),
).toBeInTheDocument();
});
test("renders sidebar description with add link", () => {
render(<RightRailPage />);
const description = screen.getByText((content, element) => {
if (element?.tagName !== "P") return false;
const text = element.textContent ?? "";
return (
text.includes("You can also combine or") &&
text.includes("add") &&
text.includes("new approaches to the list")
);
});
expect(description).toBeInTheDocument();
});
test("renders message box with title and checkboxes", () => {
render(<RightRailPage />);
const region = screen.getByRole("region", {
name: "Consider defining approaches to steward key resources:",
});
expect(region).toBeInTheDocument();
expect(
within(region).getByRole("checkbox", {
name: "Amend your CommunityRule",
}),
).toBeInTheDocument();
expect(
within(region).getByRole("checkbox", { name: "Steward finances" }),
).toBeInTheDocument();
expect(
within(region).getByRole("checkbox", { name: "Project level decisions" }),
).toBeInTheDocument();
expect(
within(region).getByRole("checkbox", {
name: "Discipline and member termination",
}),
).toBeInTheDocument();
});
test("renders card stack with See all decision approaches toggle", () => {
render(<RightRailPage />);
expect(
screen.getByRole("button", { name: "See all decision approaches" }),
).toBeInTheDocument();
});
test("renders recommended approach cards", () => {
render(<RightRailPage />);
expect(
screen.getByRole("button", {
name: /Mediation: Collaborative work to reach a resolution/,
}),
).toBeInTheDocument();
expect(
screen.getByRole("button", {
name: /Facilitated dialogue: Structured sessions/,
}),
).toBeInTheDocument();
expect(
screen.getByRole("button", {
name: /Invite-only: Private discussions with selected participants/,
}),
).toBeInTheDocument();
});
test("toggle expands and shows Show less", async () => {
const user = userEvent.setup();
render(<RightRailPage />);
const toggle = screen.getByRole("button", {
name: "See all decision approaches",
});
await user.click(toggle);
expect(
screen.getByRole("button", { name: "Show less" }),
).toBeInTheDocument();
});
test("expanded view shows Label cards", async () => {
const user = userEvent.setup();
render(<RightRailPage />);
const toggle = screen.getByRole("button", {
name: "See all decision approaches",
});
await user.click(toggle);
const labelButtons = screen.getAllByRole("button", { name: /^Label/ });
expect(labelButtons.length).toBeGreaterThanOrEqual(1);
});
test("clicking a card toggles selection", async () => {
const user = userEvent.setup();
render(<RightRailPage />);
const mediationCard = screen.getByRole("button", {
name: /Mediation: Collaborative work to reach a resolution/,
});
await user.click(mediationCard);
expect(screen.getByText("SELECTED")).toBeInTheDocument();
});
test("message box checkboxes are interactive", async () => {
const user = userEvent.setup();
render(<RightRailPage />);
const amendCheckbox = screen.getByRole("checkbox", {
name: "Amend your CommunityRule",
});
expect(amendCheckbox).not.toBeChecked();
await user.click(amendCheckbox);
expect(amendCheckbox).toBeChecked();
});
});
+4 -4
View File
@@ -15,10 +15,8 @@ vi.mock("next/dynamic", () => {
default: (importFn, options) => {
// In tests, resolve the dynamic import immediately and return the component
let Component = null;
let resolved = false;
importFn().then((mod) => {
Component = mod.default || mod;
resolved = true;
});
// Return a synchronous wrapper that uses the mocked component
return (props) => {
@@ -261,10 +259,12 @@ describe("User Journey Integration", () => {
},
{ timeout: 3000 },
);
} catch (error) {
} 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");
console.warn(
"Dynamic import for RuleStack did not resolve in test environment",
);
}
// 4. User sees features and benefits - wait for dynamically imported component
+5 -5
View File
@@ -89,10 +89,7 @@ describe("Card Component", () => {
render(<Card {...defaultProps} />);
const card = screen.getByRole("button");
expect(card).toHaveAttribute(
"aria-label",
"Label: Support text here",
);
expect(card).toHaveAttribute("aria-label", "Label: Support text here");
expect(card).toHaveAttribute("tabIndex", "0");
});
@@ -100,6 +97,9 @@ describe("Card Component", () => {
render(<Card label="Label only" orientation="horizontal" />);
expect(screen.getByText("Label only")).toBeInTheDocument();
expect(screen.getByRole("button")).toHaveAttribute("aria-label", "Label only");
expect(screen.getByRole("button")).toHaveAttribute(
"aria-label",
"Label only",
);
});
});
+22 -7
View File
@@ -9,9 +9,24 @@ import { vi, describe, test, expect, afterEach } from "vitest";
import CardStack from "../../app/components/utility/CardStack";
const SAMPLE_CARDS = [
{ id: "1", label: "Option A", supportText: "Description A", recommended: true },
{ id: "2", label: "Option B", supportText: "Description B", recommended: false },
{ id: "3", label: "Option C", supportText: "Description C", recommended: true },
{
id: "1",
label: "Option A",
supportText: "Description A",
recommended: true,
},
{
id: "2",
label: "Option B",
supportText: "Description B",
recommended: false,
},
{
id: "3",
label: "Option C",
supportText: "Description C",
recommended: true,
},
];
afterEach(() => {
@@ -62,7 +77,9 @@ describe("CardStack Component", () => {
render(<CardStack cards={SAMPLE_CARDS} hasMore={false} />);
expect(
screen.queryByRole("button", { name: "See all communication approaches" }),
screen.queryByRole("button", {
name: "See all communication approaches",
}),
).not.toBeInTheDocument();
});
@@ -82,9 +99,7 @@ describe("CardStack Component", () => {
test("calls onCardSelect when a card is clicked", () => {
const onCardSelect = vi.fn();
render(
<CardStack cards={SAMPLE_CARDS} onCardSelect={onCardSelect} />,
);
render(<CardStack cards={SAMPLE_CARDS} onCardSelect={onCardSelect} />);
const cardButtons = screen.getAllByRole("button", {
name: "Option A: Description A",
+11 -4
View File
@@ -39,7 +39,16 @@ describe("NumberCard Component", () => {
const card = screen
.getByText("Test Card Text")
.closest("div").parentElement;
expect(card).toHaveClass("flex", "flex-col", "sm:flex-row", "sm:items-center", "lg:flex-col", "lg:items-start", "lg:justify-end", "lg:relative");
expect(card).toHaveClass(
"flex",
"flex-col",
"sm:flex-row",
"sm:items-center",
"lg:flex-col",
"lg:items-start",
"lg:justify-end",
"lg:relative",
);
});
it("applies proper responsive spacing when size is not specified", () => {
@@ -194,9 +203,7 @@ describe("NumberCard Component", () => {
render(<NumberCard {...defaultProps} size="Small" />);
// For Small size, text is directly in card div (no wrapper), so use closest("div")
const card = screen
.getByText("Test Card Text")
.closest("div");
const card = screen.getByText("Test Card Text").closest("div");
expect(card).toHaveClass(
"flex",
"flex-col",
+5 -12
View File
@@ -148,7 +148,9 @@ describe("RuleCard Component", () => {
const heading = screen.getByRole("heading", { level: 3 });
// Check for responsive font classes - at 1440px+ it should have font-bricolage-grotesque and font-extrabold
expect(heading?.className).toMatch(/min-\[1440px\]:font-bricolage-grotesque/);
expect(heading?.className).toMatch(
/min-\[1440px\]:font-bricolage-grotesque/,
);
expect(heading?.className).toMatch(/min-\[1440px\]:font-extrabold/);
});
@@ -162,11 +164,7 @@ describe("RuleCard Component", () => {
},
];
render(
<RuleCard
{...defaultProps}
expanded={true}
categories={categories}
/>,
<RuleCard {...defaultProps} expanded={true} categories={categories} />,
);
expect(screen.getByText("Values")).toBeInTheDocument();
@@ -186,12 +184,7 @@ describe("RuleCard Component", () => {
});
it("renders with community initials fallback", () => {
render(
<RuleCard
{...defaultProps}
communityInitials="CE"
/>,
);
render(<RuleCard {...defaultProps} communityInitials="CE" />);
expect(screen.getByText("CE")).toBeInTheDocument();
});
+6 -2
View File
@@ -147,8 +147,12 @@ describe("RuleStack Component", () => {
"/assets/Icon_Sociocracy.svg",
);
// Check for responsive icon size classes
expect(sociocracyIcon?.className).toMatch(/min-\[640px\]:max-\[1023px\]:w-\[56px\]/);
expect(sociocracyIcon?.className).toMatch(/min-\[640px\]:max-\[1023px\]:h-\[56px\]/);
expect(sociocracyIcon?.className).toMatch(
/min-\[640px\]:max-\[1023px\]:w-\[56px\]/,
);
expect(sociocracyIcon?.className).toMatch(
/min-\[640px\]:max-\[1023px\]:h-\[56px\]/,
);
expect(sociocracyIcon?.className).toMatch(/min-\[1440px\]:w-\[90px\]/);
expect(sociocracyIcon?.className).toMatch(/min-\[1440px\]:h-\[90px\]/);
});