Simplify and standardize testing structure

This commit is contained in:
adilallo
2026-01-28 14:04:04 -07:00
parent e7a31789e3
commit 7ea724a8d9
95 changed files with 1534 additions and 15485 deletions
+72
View File
@@ -0,0 +1,72 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import AskOrganizer from "../../app/components/AskOrganizer";
import {
componentTestSuite,
ComponentTestSuiteConfig,
} from "../utils/componentTestSuite";
type AskOrganizerProps = React.ComponentProps<typeof AskOrganizer>;
const baseProps: AskOrganizerProps = {
title: "Need help?",
};
const config: ComponentTestSuiteConfig<AskOrganizerProps> = {
component: AskOrganizer,
name: "AskOrganizer",
props: baseProps,
optionalProps: {
subtitle: "Subtitle",
description: "Description",
buttonText: "Button",
buttonHref: "/link",
className: "custom",
variant: "centered",
},
primaryRole: "region",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: false,
disabledState: false,
errorState: false,
},
};
componentTestSuite<AskOrganizerProps>(config);
describe("AskOrganizer (behavioral tests)", () => {
it("renders title", () => {
render(<AskOrganizer title="Test Title" />);
expect(
screen.getByRole("heading", { name: "Test Title" }),
).toBeInTheDocument();
});
it("renders subtitle when provided", () => {
render(<AskOrganizer title="Test" subtitle="Subtitle" />);
expect(screen.getByRole("heading", { name: "Subtitle" })).toBeInTheDocument();
});
it("renders button with default text", () => {
render(<AskOrganizer title="Test" />);
expect(
screen.getByRole("link", {
name: /ask an organizer/i,
}),
).toBeInTheDocument();
});
it("renders button with custom text", () => {
render(
<AskOrganizer title="Test" buttonText="Contact" buttonHref="/contact" />,
);
expect(
screen.getByRole("link", {
name: /contact/i,
}),
).toBeInTheDocument();
});
});
+66
View File
@@ -0,0 +1,66 @@
import React from "react";
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom/vitest";
import Button from "../../app/components/Button";
import {
componentTestSuite,
ComponentTestSuiteConfig,
} from "../utils/componentTestSuite";
type ButtonProps = React.ComponentProps<typeof Button>;
const baseProps: ButtonProps = {
children: "Click me",
};
const config: ComponentTestSuiteConfig<ButtonProps> = {
component: Button,
name: "Button",
props: baseProps,
requiredProps: ["children"],
optionalProps: {
href: "/test",
ariaLabel: "Accessible button",
},
primaryRole: "button",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: false,
},
states: {
disabledProps: { disabled: true },
},
};
componentTestSuite<ButtonProps>(config);
describe("Button (behavioral tests)", () => {
it("calls onClick when clicked", async () => {
const user = userEvent.setup();
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
const button = screen.getByRole("button", { name: "Click me" });
await user.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
});
it("renders as a link when href is provided", () => {
render(
<Button href="/learn" variant="default">
Learn more
</Button>,
);
const link = screen.getByRole("link", { name: "Learn more" });
expect(link).toHaveAttribute("href", "/learn");
});
});
+29
View File
@@ -0,0 +1,29 @@
import React from "react";
import Checkbox from "../../app/components/Checkbox";
import { componentTestSuite } from "../utils/componentTestSuite";
type CheckboxProps = React.ComponentProps<typeof Checkbox>;
componentTestSuite<CheckboxProps>({
component: Checkbox,
name: "Checkbox",
props: {
label: "Test checkbox",
} as CheckboxProps,
requiredProps: ["label"],
optionalProps: {
value: "test",
},
primaryRole: "checkbox",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: false,
},
states: {
disabledProps: { disabled: true },
},
});
+45
View File
@@ -0,0 +1,45 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import ContentBanner from "../../app/components/ContentBanner";
import type { BlogPost } from "../../lib/content";
vi.mock("next/link", () => ({
default: ({ children, href, ...props }: any) => (
<a href={href} {...props}>
{children}
</a>
),
}));
vi.mock("../../lib/assetUtils", () => ({
getAssetPath: vi.fn((asset: string) => `/assets/${asset}`),
}));
const mockPost: BlogPost = {
slug: "test-article",
frontmatter: {
title: "Test Article",
description: "Test description",
author: "Test Author",
date: "2025-04-15",
},
};
describe("ContentBanner", () => {
it("renders without crashing", () => {
render(<ContentBanner post={mockPost} />);
});
it("renders article title", () => {
render(<ContentBanner post={mockPost} />);
expect(
screen.getByRole("heading", { name: "Test Article" }),
).toBeInTheDocument();
});
it("renders article description", () => {
render(<ContentBanner post={mockPost} />);
expect(screen.getByText("Test description")).toBeInTheDocument();
});
});
+28
View File
@@ -0,0 +1,28 @@
import React from "react";
import ContextMenu from "../../app/components/ContextMenu";
import ContextMenuItem from "../../app/components/ContextMenuItem";
import { componentTestSuite } from "../utils/componentTestSuite";
type ContextMenuProps = React.ComponentProps<typeof ContextMenu>;
componentTestSuite<ContextMenuProps>({
component: ContextMenu,
name: "ContextMenu",
props: {
children: (
<ContextMenuItem>
Item
</ContextMenuItem>
),
} as ContextMenuProps,
requiredProps: [],
primaryRole: "menu",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: false,
disabledState: false,
errorState: false,
},
});
+26
View File
@@ -0,0 +1,26 @@
import React from "react";
import ContextMenuItem from "../../app/components/ContextMenuItem";
import { componentTestSuite } from "../utils/componentTestSuite";
type ContextMenuItemProps = React.ComponentProps<typeof ContextMenuItem>;
componentTestSuite<ContextMenuItemProps>({
component: ContextMenuItem,
name: "ContextMenuItem",
props: {
children: "Item",
} as ContextMenuItemProps,
requiredProps: [],
primaryRole: "menuitem",
testCases: {
renders: true,
accessibility: false,
keyboardNavigation: true,
disabledState: true,
errorState: false,
},
states: {
disabledProps: { disabled: true },
},
});
+80
View File
@@ -0,0 +1,80 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import FeatureGrid from "../../app/components/FeatureGrid";
import {
componentTestSuite,
ComponentTestSuiteConfig,
} from "../utils/componentTestSuite";
type FeatureGridProps = React.ComponentProps<typeof FeatureGrid>;
const baseProps: FeatureGridProps = {
title: "Feature Tools",
subtitle: "Everything you need",
};
const config: ComponentTestSuiteConfig<FeatureGridProps> = {
component: FeatureGrid,
name: "FeatureGrid",
props: baseProps,
optionalProps: {
className: "custom-class",
title: undefined,
subtitle: undefined,
},
primaryRole: "region",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: false,
disabledState: false,
errorState: false,
},
};
componentTestSuite<FeatureGridProps>(config);
describe("FeatureGrid (behavioral tests)", () => {
it("renders title and subtitle", () => {
render(<FeatureGrid title="Test Title" subtitle="Test Subtitle" />);
expect(
screen.getByRole("heading", { name: "Test Title" }),
).toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "Test Subtitle" }),
).toBeInTheDocument();
});
it("renders all four feature cards", () => {
render(<FeatureGrid title="Test" subtitle="Test" />);
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();
});
it("has proper accessibility attributes", () => {
render(<FeatureGrid title="Test" subtitle="Test" />);
const section = document.querySelector("section");
expect(section).toHaveAttribute("aria-labelledby", "feature-grid-headline");
expect(screen.getByRole("grid")).toHaveAttribute(
"aria-label",
"Feature tools and services",
);
});
it("handles missing props gracefully", () => {
render(<FeatureGrid />);
const section = document.querySelector("section");
expect(section).toBeInTheDocument();
});
});
+73
View File
@@ -0,0 +1,73 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import Footer from "../../app/components/Footer";
import {
componentTestSuite,
ComponentTestSuiteConfig,
} from "../utils/componentTestSuite";
type FooterProps = React.ComponentProps<typeof Footer>;
const baseProps: FooterProps = {};
const config: ComponentTestSuiteConfig<FooterProps> = {
component: Footer,
name: "Footer",
props: baseProps,
primaryRole: "contentinfo",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: false, // Footer is not primarily keyboard navigable
disabledState: false,
errorState: false,
},
};
componentTestSuite<FooterProps>(config);
describe("Footer (behavioral tests)", () => {
it("renders organization schema markup", () => {
render(<Footer />);
const script = document.querySelector('script[type="application/ld+json"]');
expect(script).toBeInTheDocument();
const schemaData = JSON.parse(script?.textContent || "{}");
expect(schemaData["@type"]).toBe("Organization");
expect(schemaData.name).toBe("Media Economies Design Lab");
});
it("renders organization name and contact", () => {
render(<Footer />);
expect(
screen.getAllByText("Media Economies Design Lab").length,
).toBeGreaterThan(0);
expect(
screen.getAllByRole("link", { name: "medlab@colorado.edu" }).length,
).toBeGreaterThan(0);
});
it("renders social media links", () => {
render(<Footer />);
expect(
screen.getAllByRole("link", { name: "Follow us on Bluesky" }).length,
).toBeGreaterThan(0);
expect(
screen.getAllByRole("link", { name: "Follow us on GitLab" }).length,
).toBeGreaterThan(0);
});
it("renders navigation links", () => {
render(<Footer />);
expect(screen.getAllByRole("link", { name: "Use cases" }).length).toBeGreaterThan(0);
expect(screen.getAllByRole("link", { name: "Learn" }).length).toBeGreaterThan(0);
expect(screen.getAllByRole("link", { name: "About" }).length).toBeGreaterThan(0);
});
it("renders legal links", () => {
render(<Footer />);
expect(screen.getAllByRole("link", { name: "Privacy Policy" }).length).toBeGreaterThan(0);
expect(screen.getAllByRole("link", { name: "Terms of Service" }).length).toBeGreaterThan(0);
});
});
+22
View File
@@ -0,0 +1,22 @@
import React from "react";
import Header from "../../app/components/Header";
import { componentTestSuite } from "../utils/componentTestSuite";
type HeaderProps = React.ComponentProps<typeof Header>;
componentTestSuite<HeaderProps>({
component: Header,
name: "Header",
// Header has no props; it reads from routing and config.
props: {} as HeaderProps,
requiredProps: [],
primaryRole: "banner",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: false,
disabledState: false,
errorState: false,
},
});
+66
View File
@@ -0,0 +1,66 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import HeroBanner from "../../app/components/HeroBanner";
import {
componentTestSuite,
ComponentTestSuiteConfig,
} from "../utils/componentTestSuite";
type HeroBannerProps = React.ComponentProps<typeof HeroBanner>;
const baseProps: HeroBannerProps = {
title: "Welcome",
};
const config: ComponentTestSuiteConfig<HeroBannerProps> = {
component: HeroBanner,
name: "HeroBanner",
props: baseProps,
optionalProps: {
subtitle: "Subtitle",
description: "Description",
ctaText: "CTA",
ctaHref: "/link",
},
primaryRole: "region",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: false,
disabledState: false,
errorState: false,
},
};
componentTestSuite<HeroBannerProps>(config);
describe("HeroBanner (behavioral tests)", () => {
it("renders title", () => {
render(<HeroBanner title="Test Title" />);
expect(
screen.getByRole("heading", { name: "Test Title" }),
).toBeInTheDocument();
});
it("renders subtitle when provided", () => {
render(<HeroBanner title="Test" subtitle="Subtitle" />);
expect(screen.getByRole("heading", { name: "Subtitle" })).toBeInTheDocument();
});
it("renders hero image", () => {
render(<HeroBanner title="Test" />);
expect(
screen.getByRole("img", { name: "Hero illustration" }),
).toBeInTheDocument();
});
it("renders CTA button when provided", () => {
render(
<HeroBanner title="Test" ctaText="Get Started" ctaHref="/start" />,
);
expect(
screen.getAllByRole("button", { name: "Get Started" }).length,
).toBeGreaterThan(0);
});
});
+30
View File
@@ -0,0 +1,30 @@
import React from "react";
import Input from "../../app/components/Input";
import { componentTestSuite } from "../utils/componentTestSuite";
type InputProps = React.ComponentProps<typeof Input>;
componentTestSuite<InputProps>({
component: Input,
name: "Input",
props: {
label: "Test input",
} as InputProps,
requiredProps: ["label"],
optionalProps: {
placeholder: "Enter value",
},
primaryRole: "textbox",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: true,
},
states: {
disabledProps: { disabled: true },
errorProps: { error: true },
},
});
+68
View File
@@ -0,0 +1,68 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import Logo from "../../app/components/Logo";
import {
componentTestSuite,
ComponentTestSuiteConfig,
} from "../utils/componentTestSuite";
type LogoProps = React.ComponentProps<typeof Logo>;
const baseProps: LogoProps = {};
const config: ComponentTestSuiteConfig<LogoProps> = {
component: Logo,
name: "Logo",
props: baseProps,
primaryRole: "link",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: false,
errorState: false,
},
};
componentTestSuite<LogoProps>(config);
describe("Logo (behavioral tests)", () => {
it("renders as a link to home", () => {
render(<Logo />);
const logo = screen.getByRole("link", { name: /communityrule logo/i });
expect(logo).toHaveAttribute("href", "/");
expect(logo).toHaveAttribute("aria-label", "CommunityRule Logo");
});
it("renders logo icon", () => {
render(<Logo />);
expect(
screen.getByAltText("CommunityRule Logo Icon"),
).toBeInTheDocument();
});
it("renders text by default", () => {
render(<Logo />);
expect(screen.getByText("CommunityRule")).toBeInTheDocument();
});
it("hides text when showText is false", () => {
render(<Logo showText={false} />);
expect(screen.queryByText("CommunityRule")).not.toBeInTheDocument();
expect(
screen.getByAltText("CommunityRule Logo Icon"),
).toBeInTheDocument();
});
it("renders with different size variants", () => {
const { rerender } = render(<Logo size="header" />);
expect(screen.getByRole("link")).toBeInTheDocument();
rerender(<Logo size="footer" />);
expect(screen.getByRole("link")).toBeInTheDocument();
rerender(<Logo size="homeHeaderMd" />);
expect(screen.getByRole("link")).toBeInTheDocument();
});
});
+30
View File
@@ -0,0 +1,30 @@
import React from "react";
import RadioButton from "../../app/components/RadioButton";
import { componentTestSuite } from "../utils/componentTestSuite";
type RadioButtonProps = React.ComponentProps<typeof RadioButton>;
componentTestSuite<RadioButtonProps>({
component: RadioButton,
name: "RadioButton",
props: {
label: "Option A",
checked: false,
} as RadioButtonProps,
requiredProps: [],
optionalProps: {
mode: "inverse",
},
primaryRole: "radio",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: false,
},
states: {
disabledProps: { disabled: true },
},
});
+28
View File
@@ -0,0 +1,28 @@
import React from "react";
import RadioGroup from "../../app/components/RadioGroup";
import { componentTestSuite } from "../utils/componentTestSuite";
type RadioGroupProps = React.ComponentProps<typeof RadioGroup>;
componentTestSuite<RadioGroupProps>({
component: RadioGroup,
name: "RadioGroup",
props: {
name: "example",
value: "a",
options: [
{ value: "a", label: "Option A" },
{ value: "b", label: "Option B" },
],
} as RadioGroupProps,
requiredProps: [],
primaryRole: "radiogroup",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: false,
disabledState: false,
errorState: false,
},
});
+81
View File
@@ -0,0 +1,81 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import RelatedArticles from "../../app/components/RelatedArticles";
import type { BlogPost } from "../../lib/content";
vi.mock("next/link", () => ({
default: ({ children, href, ...props }: any) => (
<a href={href} {...props}>
{children}
</a>
),
}));
vi.mock("../../app/components/ContentThumbnailTemplate", () => ({
default: ({ post }: { post: BlogPost }) => (
<div data-testid={`thumbnail-${post.slug}`}>
<a href={`/blog/${post.slug}`}>
<h3>{post.frontmatter.title}</h3>
</a>
</div>
),
}));
vi.mock("../../app/hooks", () => ({
useIsMobile: () => false,
}));
const mockPosts: BlogPost[] = [
{
slug: "article-1",
frontmatter: {
title: "Article 1",
description: "Description 1",
author: "Author",
date: "2025-04-10",
},
},
{
slug: "article-2",
frontmatter: {
title: "Article 2",
description: "Description 2",
author: "Author",
date: "2025-04-11",
},
},
];
describe("RelatedArticles", () => {
it("renders without crashing", () => {
render(
<RelatedArticles
relatedPosts={mockPosts}
currentPostSlug="current"
/>,
);
});
it("renders related articles", () => {
render(
<RelatedArticles
relatedPosts={mockPosts}
currentPostSlug="current"
/>,
);
expect(screen.getByTestId("thumbnail-article-1")).toBeInTheDocument();
expect(screen.getByTestId("thumbnail-article-2")).toBeInTheDocument();
});
it("filters out current post", () => {
render(
<RelatedArticles
relatedPosts={mockPosts}
currentPostSlug="article-1"
/>,
);
expect(screen.queryByTestId("thumbnail-article-1")).not.toBeInTheDocument();
expect(screen.getByTestId("thumbnail-article-2")).toBeInTheDocument();
});
});
+24
View File
@@ -0,0 +1,24 @@
import React from "react";
import SectionHeader from "../../app/components/SectionHeader";
import { componentTestSuite } from "../utils/componentTestSuite";
type SectionHeaderProps = React.ComponentProps<typeof SectionHeader>;
componentTestSuite<SectionHeaderProps>({
component: SectionHeader,
name: "SectionHeader",
props: {
title: "Title",
subtitle: "Subtitle",
} as SectionHeaderProps,
requiredProps: ["title", "subtitle"],
primaryRole: "heading",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: false,
disabledState: false,
errorState: false,
},
});
+35
View File
@@ -0,0 +1,35 @@
import React from "react";
import Select from "../../app/components/Select";
import { componentTestSuite } from "../utils/componentTestSuite";
type SelectProps = React.ComponentProps<typeof Select>;
componentTestSuite<SelectProps>({
component: Select,
name: "Select",
props: {
label: "Test Select",
placeholder: "Select an option",
options: [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
],
} as SelectProps,
requiredProps: ["options"],
optionalProps: {
size: "medium",
},
primaryRole: "button",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: true,
},
states: {
disabledProps: { disabled: true },
errorProps: { error: true },
},
});
+29
View File
@@ -0,0 +1,29 @@
import React from "react";
import Switch from "../../app/components/Switch";
import { componentTestSuite } from "../utils/componentTestSuite";
type SwitchProps = React.ComponentProps<typeof Switch>;
componentTestSuite<SwitchProps>({
component: Switch,
name: "Switch",
props: {
label: "Test Switch",
} as SwitchProps,
requiredProps: [],
optionalProps: {
state: "focus",
},
primaryRole: "switch",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: false,
},
states: {
disabledProps: { disabled: true },
},
});
+31
View File
@@ -0,0 +1,31 @@
import React from "react";
import TextArea from "../../app/components/TextArea";
import { componentTestSuite } from "../utils/componentTestSuite";
type TextAreaProps = React.ComponentProps<typeof TextArea>;
componentTestSuite<TextAreaProps>({
component: TextArea,
name: "TextArea",
props: {
label: "Description",
value: "",
} as TextAreaProps,
requiredProps: ["label"],
optionalProps: {
placeholder: "Enter description",
},
primaryRole: "textbox",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: true,
},
states: {
disabledProps: { disabled: true },
errorProps: { error: true },
},
});
+27
View File
@@ -0,0 +1,27 @@
import React from "react";
import Toggle from "../../app/components/Toggle";
import { componentTestSuite } from "../utils/componentTestSuite";
type ToggleProps = React.ComponentProps<typeof Toggle>;
componentTestSuite<ToggleProps>({
component: Toggle,
name: "Toggle",
props: {
label: "Notifications",
checked: false,
} as ToggleProps,
requiredProps: [],
primaryRole: "switch",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: false,
},
states: {
disabledProps: { disabled: true },
},
});
+23
View File
@@ -0,0 +1,23 @@
import React from "react";
import ToggleGroup from "../../app/components/ToggleGroup";
import { componentTestSuite } from "../utils/componentTestSuite";
type ToggleGroupProps = React.ComponentProps<typeof ToggleGroup>;
componentTestSuite<ToggleGroupProps>({
component: ToggleGroup,
name: "ToggleGroup",
props: {
children: "Option",
} as ToggleGroupProps,
requiredProps: [],
primaryRole: "button",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: false,
errorState: false,
},
});