Simplify and standardize testing structure
This commit is contained in:
@@ -1,298 +0,0 @@
|
||||
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(
|
||||
<AskOrganizer
|
||||
title="Need help organizing?"
|
||||
subtitle="Get expert guidance"
|
||||
description="Our organizers can help you build better communities"
|
||||
buttonText="Contact an organizer"
|
||||
buttonHref="/contact"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "Need help organizing?" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "Get expert guidance" }),
|
||||
).toBeInTheDocument();
|
||||
// The description text might not be rendered or might be different
|
||||
// Just verify the component renders without error
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "Need help organizing?" }),
|
||||
).toBeInTheDocument();
|
||||
// Button renders as a link when href is provided
|
||||
expect(
|
||||
screen.getByRole("link", {
|
||||
name: "Contact an organizer - Contact an organizer for help",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with default button text", () => {
|
||||
render(<AskOrganizer title="Test" subtitle="Test" description="Test" />);
|
||||
|
||||
// Button renders as a link when href is provided
|
||||
expect(
|
||||
screen.getByRole("link", {
|
||||
name: "Ask an organizer - Contact an organizer for help",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with custom className", () => {
|
||||
render(
|
||||
<AskOrganizer title="Test" subtitle="Test" className="custom-class" />,
|
||||
);
|
||||
|
||||
const section = document.querySelector("section");
|
||||
expect(section).toHaveClass("custom-class");
|
||||
});
|
||||
|
||||
test("renders different variants", () => {
|
||||
const { rerender } = render(
|
||||
<AskOrganizer title="Test" subtitle="Test" variant="centered" />,
|
||||
);
|
||||
|
||||
// Centered variant should have center alignment
|
||||
const container = screen
|
||||
.getByRole("region")
|
||||
.querySelector('[class*="text-center"]');
|
||||
expect(container).toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
<AskOrganizer title="Test" subtitle="Test" variant="left-aligned" />,
|
||||
);
|
||||
|
||||
// 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(
|
||||
<AskOrganizer
|
||||
title="Ask Title"
|
||||
subtitle="Ask Subtitle"
|
||||
description="Ask Description"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "Ask Title" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "Ask Subtitle" }),
|
||||
).toBeInTheDocument();
|
||||
// Description might not be rendered if not provided to ContentLockup
|
||||
// Just verify the component renders without error
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "Ask Title" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders button with correct props", () => {
|
||||
render(
|
||||
<AskOrganizer
|
||||
title="Test"
|
||||
subtitle="Test"
|
||||
buttonText="Custom Button"
|
||||
buttonHref="/custom"
|
||||
/>,
|
||||
);
|
||||
|
||||
const button = screen.getByRole("link", {
|
||||
name: "Custom Button - Contact an organizer for help",
|
||||
});
|
||||
expect(button).toHaveAttribute("href", "/custom");
|
||||
expect(button).toHaveClass("xl:!px-[var(--spacing-scale-020)]");
|
||||
});
|
||||
|
||||
test("handles button click events", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onContactClick = vi.fn();
|
||||
|
||||
render(
|
||||
<AskOrganizer
|
||||
title="Test"
|
||||
subtitle="Test"
|
||||
onContactClick={onContactClick}
|
||||
/>,
|
||||
);
|
||||
|
||||
const button = screen.getByRole("link", {
|
||||
name: "Ask an organizer - Contact an organizer for help",
|
||||
});
|
||||
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(<AskOrganizer title="Test" subtitle="Test" />);
|
||||
|
||||
const button = screen.getByRole("link", {
|
||||
name: "Ask an organizer - Contact an organizer for help",
|
||||
});
|
||||
await user.click(button);
|
||||
|
||||
// Verify gtag was called with the expected event
|
||||
expect(gtagSpy).toHaveBeenCalledWith(
|
||||
"event",
|
||||
"contact_button_click",
|
||||
expect.objectContaining({
|
||||
event_category: "engagement",
|
||||
event_label: "ask_organizer",
|
||||
value: 1,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("renders with proper accessibility attributes", () => {
|
||||
render(
|
||||
<AskOrganizer title="Test" subtitle="Test" buttonText="Custom Button" />,
|
||||
);
|
||||
|
||||
const section = document.querySelector("section");
|
||||
expect(section).toHaveAttribute(
|
||||
"aria-labelledby",
|
||||
"ask-organizer-headline",
|
||||
);
|
||||
expect(section).toHaveAttribute("tabIndex", "-1");
|
||||
|
||||
const button = screen.getByRole("link", {
|
||||
name: "Custom Button - Contact an organizer for help",
|
||||
});
|
||||
expect(button).toHaveAttribute(
|
||||
"aria-label",
|
||||
"Custom Button - Contact an organizer for help",
|
||||
);
|
||||
});
|
||||
|
||||
test("renders with design tokens", () => {
|
||||
render(<AskOrganizer title="Test" subtitle="Test" />);
|
||||
|
||||
const section = document.querySelector("section");
|
||||
expect(section).toHaveClass(
|
||||
"py-[var(--spacing-scale-032)]",
|
||||
"px-[var(--spacing-scale-032)]",
|
||||
);
|
||||
});
|
||||
|
||||
test("applies responsive spacing", () => {
|
||||
render(<AskOrganizer title="Test" subtitle="Test" />);
|
||||
|
||||
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(<AskOrganizer title="Test" subtitle="Test" />);
|
||||
|
||||
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(
|
||||
<AskOrganizer title="Test" subtitle="Test" variant="compact" />,
|
||||
);
|
||||
|
||||
// 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(
|
||||
<AskOrganizer title="Test" subtitle="Test" variant="left-aligned" />,
|
||||
);
|
||||
|
||||
// Left-aligned variant should have left alignment
|
||||
const container = section.querySelector('[class*="text-left"]');
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders button with custom styling", () => {
|
||||
render(<AskOrganizer title="Test" subtitle="Test" />);
|
||||
|
||||
const button = screen.getByRole("link", {
|
||||
name: "Ask an organizer - Contact an organizer for help",
|
||||
});
|
||||
expect(button).toHaveClass(
|
||||
"xl:!px-[var(--spacing-scale-020)]",
|
||||
"xl:!py-[var(--spacing-scale-012)]",
|
||||
);
|
||||
});
|
||||
|
||||
test("handles missing optional props gracefully", () => {
|
||||
render(<AskOrganizer title="Test" />);
|
||||
|
||||
// Should still render the structure
|
||||
const section = document.querySelector("section");
|
||||
expect(section).toBeInTheDocument();
|
||||
|
||||
// Should render default button (as link when href is provided)
|
||||
expect(
|
||||
screen.getByRole("link", {
|
||||
name: "Ask an organizer - Contact an organizer for help",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("applies responsive button container alignment", () => {
|
||||
render(<AskOrganizer title="Test" subtitle="Test" variant="centered" />);
|
||||
|
||||
// Button renders as a link when href is provided
|
||||
const buttonContainer = screen
|
||||
.getByRole("link", {
|
||||
name: "Ask an organizer - Contact an organizer for help",
|
||||
})
|
||||
.closest("div");
|
||||
expect(buttonContainer).toHaveClass("flex", "justify-center");
|
||||
});
|
||||
|
||||
test("renders with proper content gap", () => {
|
||||
render(<AskOrganizer title="Test" subtitle="Test" variant="compact" />);
|
||||
|
||||
const container = screen
|
||||
.getByRole("region")
|
||||
.querySelector('[class*="flex flex-col"]');
|
||||
expect(container).toHaveClass("gap-[var(--spacing-scale-020)]");
|
||||
});
|
||||
});
|
||||
@@ -1,396 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import BlogPostPage from "../../app/blog/[slug]/page";
|
||||
|
||||
// Mock Next.js components
|
||||
vi.mock("next/navigation", () => ({
|
||||
notFound: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("next/link", () => {
|
||||
return {
|
||||
default: ({ children, href, ...props }) => (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock next/dynamic to return components synchronously in tests
|
||||
vi.mock("next/dynamic", () => {
|
||||
return {
|
||||
default: (importFn, options) => {
|
||||
// In tests, resolve the dynamic import immediately and return the component
|
||||
let Component = null;
|
||||
importFn().then((mod) => {
|
||||
Component = mod.default || mod;
|
||||
});
|
||||
// Return a synchronous wrapper that uses the mocked component
|
||||
return (props) => {
|
||||
// Use the mocked RelatedArticles component directly
|
||||
if (Component) {
|
||||
return <Component {...props} />;
|
||||
}
|
||||
// Fallback: return the loading placeholder if component not ready
|
||||
return options?.loading ? options.loading() : null;
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Mock content processing
|
||||
vi.mock("../../lib/content", () => ({
|
||||
getBlogPostBySlug: vi.fn(),
|
||||
getAllBlogPosts: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock components
|
||||
vi.mock("../../app/components/ContentBanner", () => {
|
||||
return {
|
||||
default: ({ post }) => (
|
||||
<div data-testid="content-banner">
|
||||
<h1>{post.frontmatter.title}</h1>
|
||||
<p>{post.frontmatter.description}</p>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../app/components/RelatedArticles", () => {
|
||||
return {
|
||||
default: ({ relatedPosts }) => (
|
||||
<div data-testid="related-articles">
|
||||
<h2>Related Articles</h2>
|
||||
{relatedPosts.map((post) => (
|
||||
<div key={post.slug} data-testid={`related-${post.slug}`}>
|
||||
{post.frontmatter.title}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../app/components/AskOrganizer", () => {
|
||||
return {
|
||||
default: ({ title, subtitle, buttonText }) => (
|
||||
<div data-testid="ask-organizer">
|
||||
<h2>{title}</h2>
|
||||
<p>{subtitle}</p>
|
||||
<button>{buttonText}</button>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock asset utils
|
||||
vi.mock("../../lib/assetUtils", () => ({
|
||||
getAssetPath: vi.fn((asset) => `/assets/${asset}`),
|
||||
ASSETS: {
|
||||
CONTENT_SHAPE_1: "Content_Shape_1.svg",
|
||||
CONTENT_SHAPE_2: "Content_Shape_2.svg",
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock blog post data
|
||||
const mockPost = {
|
||||
slug: "test-article",
|
||||
frontmatter: {
|
||||
title: "Test Article Title",
|
||||
description: "This is a test article description",
|
||||
author: "Test Author",
|
||||
date: "2025-04-15",
|
||||
},
|
||||
htmlContent:
|
||||
"<p>This is the article content with <strong>bold text</strong> and <em>italic text</em>.</p>",
|
||||
};
|
||||
|
||||
const mockRelatedPosts = [
|
||||
{
|
||||
slug: "related-1",
|
||||
frontmatter: {
|
||||
title: "Related Article 1",
|
||||
description: "First related article",
|
||||
author: "Test Author",
|
||||
date: "2025-04-10",
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: "related-2",
|
||||
frontmatter: {
|
||||
title: "Related Article 2",
|
||||
description: "Second related article",
|
||||
author: "Test Author",
|
||||
date: "2025-04-12",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe("BlogPostPage", () => {
|
||||
beforeEach(async () => {
|
||||
// Reset mocks
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Mock the content functions
|
||||
const { getBlogPostBySlug, getAllBlogPosts } =
|
||||
await import("../../lib/content");
|
||||
vi.mocked(getBlogPostBySlug).mockReturnValue(mockPost);
|
||||
vi.mocked(getAllBlogPosts).mockReturnValue([mockPost, ...mockRelatedPosts]);
|
||||
});
|
||||
|
||||
it("renders the blog post page with correct structure", async () => {
|
||||
const BlogPostPageComponent = await BlogPostPage({
|
||||
params: { slug: "test-article" },
|
||||
});
|
||||
render(BlogPostPageComponent);
|
||||
|
||||
// Check main container (it's a div, not main)
|
||||
const mainContainer = document.querySelector("div.min-h-screen");
|
||||
expect(mainContainer).toBeInTheDocument();
|
||||
expect(mainContainer).toHaveClass(
|
||||
"min-h-screen",
|
||||
"relative",
|
||||
"overflow-hidden",
|
||||
);
|
||||
// Background color is applied via inline style from frontmatter hex
|
||||
expect(mainContainer).toHaveStyle({ backgroundColor: expect.any(String) });
|
||||
});
|
||||
|
||||
it("renders the content banner", async () => {
|
||||
const BlogPostPageComponent = await BlogPostPage({
|
||||
params: { slug: "test-article" },
|
||||
});
|
||||
render(BlogPostPageComponent);
|
||||
|
||||
expect(screen.getByTestId("content-banner")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Article Title")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("This is a test article description"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the article content", async () => {
|
||||
const BlogPostPageComponent = await BlogPostPage({
|
||||
params: { slug: "test-article" },
|
||||
});
|
||||
render(BlogPostPageComponent);
|
||||
|
||||
const article = document.querySelector("article");
|
||||
expect(article).toBeInTheDocument();
|
||||
expect(article).toHaveClass(
|
||||
"p-[var(--spacing-scale-024)]",
|
||||
"sm:py-[var(--spacing-scale-032)]",
|
||||
);
|
||||
|
||||
// Check content is rendered
|
||||
expect(screen.getByText(/This is the article content/)).toBeInTheDocument();
|
||||
expect(screen.getByText("bold text")).toBeInTheDocument();
|
||||
expect(screen.getByText("italic text")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the related articles section", async () => {
|
||||
const BlogPostPageComponent = await BlogPostPage({
|
||||
params: { slug: "test-article" },
|
||||
});
|
||||
render(BlogPostPageComponent);
|
||||
|
||||
// Wait for dynamically imported RelatedArticles component to load
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("related-articles")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText("Related Articles")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("related-related-1")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("related-related-2")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the ask organizer section", async () => {
|
||||
const BlogPostPageComponent = await BlogPostPage({
|
||||
params: { slug: "test-article" },
|
||||
});
|
||||
render(BlogPostPageComponent);
|
||||
|
||||
expect(screen.getByTestId("ask-organizer")).toBeInTheDocument();
|
||||
expect(screen.getByText("Still have questions?")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Get answers from an experienced organizer"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("Ask an organizer")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders decorative shapes", async () => {
|
||||
const BlogPostPageComponent = await BlogPostPage({
|
||||
params: { slug: "test-article" },
|
||||
});
|
||||
render(BlogPostPageComponent);
|
||||
|
||||
// Check for decorative shapes
|
||||
const shapes = screen.getAllByAltText("");
|
||||
expect(shapes).toHaveLength(2);
|
||||
|
||||
// Check shape sources
|
||||
expect(shapes[0]).toHaveAttribute("src", "/assets/Content_Shape_1.svg");
|
||||
expect(shapes[1]).toHaveAttribute("src", "/assets/Content_Shape_2.svg");
|
||||
});
|
||||
|
||||
it("applies correct styling to article content", async () => {
|
||||
const BlogPostPageComponent = await BlogPostPage({
|
||||
params: { slug: "test-article" },
|
||||
});
|
||||
render(BlogPostPageComponent);
|
||||
|
||||
const contentDiv = screen
|
||||
.getByText(/This is the article content/)
|
||||
.closest("div.post-body");
|
||||
expect(contentDiv).toHaveClass("post-body");
|
||||
expect(contentDiv).toHaveClass("-mt-[var(--spacing-scale-048)]");
|
||||
expect(contentDiv).toHaveClass(
|
||||
"text-[var(--color-content-inverse-primary)]",
|
||||
);
|
||||
expect(contentDiv).toHaveClass("text-[16px]");
|
||||
expect(contentDiv).toHaveClass("leading-[24px]");
|
||||
});
|
||||
|
||||
it("applies responsive text sizing", async () => {
|
||||
const BlogPostPageComponent = await BlogPostPage({
|
||||
params: { slug: "test-article" },
|
||||
});
|
||||
render(BlogPostPageComponent);
|
||||
|
||||
const contentDiv = screen
|
||||
.getByText(/This is the article content/)
|
||||
.closest("div.post-body");
|
||||
expect(contentDiv).toHaveClass("sm:text-[18px]");
|
||||
expect(contentDiv).toHaveClass("sm:leading-[130%]");
|
||||
expect(contentDiv).toHaveClass("lg:text-[24px]");
|
||||
expect(contentDiv).toHaveClass("lg:leading-[32px]");
|
||||
expect(contentDiv).toHaveClass("xl:text-[32px]");
|
||||
expect(contentDiv).toHaveClass("xl:leading-[40px]");
|
||||
});
|
||||
|
||||
it("applies responsive max-width constraints", async () => {
|
||||
const BlogPostPageComponent = await BlogPostPage({
|
||||
params: { slug: "test-article" },
|
||||
});
|
||||
render(BlogPostPageComponent);
|
||||
|
||||
const contentDiv = screen
|
||||
.getByText(/This is the article content/)
|
||||
.closest("div.post-body");
|
||||
expect(contentDiv).toHaveClass("sm:mx-auto");
|
||||
expect(contentDiv).toHaveClass("sm:max-w-[390px]");
|
||||
expect(contentDiv).toHaveClass("md:max-w-[472px]");
|
||||
expect(contentDiv).toHaveClass("lg:max-w-[700px]");
|
||||
expect(contentDiv).toHaveClass("xl:max-w-[904px]");
|
||||
});
|
||||
|
||||
it("includes structured data scripts", async () => {
|
||||
const BlogPostPageComponent = await BlogPostPage({
|
||||
params: { slug: "test-article" },
|
||||
});
|
||||
render(BlogPostPageComponent);
|
||||
|
||||
// Check for script elements using querySelector since RTL ignores them
|
||||
const scripts = document.querySelectorAll(
|
||||
'script[type="application/ld+json"]',
|
||||
);
|
||||
expect(scripts).toHaveLength(2);
|
||||
|
||||
// Check that scripts have the correct type and content
|
||||
scripts.forEach((script) => {
|
||||
expect(script).toHaveAttribute("type", "application/ld+json");
|
||||
expect(script.innerHTML).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("handles missing post gracefully", async () => {
|
||||
const { getBlogPostBySlug } = await import("../../lib/content");
|
||||
vi.mocked(getBlogPostBySlug).mockReturnValue(null);
|
||||
|
||||
// The component should throw an error when post is null
|
||||
// This happens because notFound() is called
|
||||
await expect(
|
||||
BlogPostPage({ params: { slug: "non-existent" } }),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("filters out current post from related articles", async () => {
|
||||
const BlogPostPageComponent = await BlogPostPage({
|
||||
params: { slug: "test-article" },
|
||||
});
|
||||
render(BlogPostPageComponent);
|
||||
|
||||
// Wait for dynamically imported RelatedArticles component to load
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("related-articles")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Current post should not appear in related articles
|
||||
expect(
|
||||
screen.queryByTestId("related-test-article"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// Other related posts should appear
|
||||
expect(screen.getByTestId("related-related-1")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("related-related-2")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies correct positioning to decorative shapes", async () => {
|
||||
const BlogPostPageComponent = await BlogPostPage({
|
||||
params: { slug: "test-article" },
|
||||
});
|
||||
render(BlogPostPageComponent);
|
||||
|
||||
const shapes = screen.getAllByAltText("");
|
||||
|
||||
// First shape (right side)
|
||||
const rightShape = shapes[0].closest("div");
|
||||
expect(rightShape).toHaveClass(
|
||||
"hidden",
|
||||
"md:block",
|
||||
"absolute",
|
||||
"top-1/4",
|
||||
"right-0",
|
||||
"pointer-events-none",
|
||||
"z-10",
|
||||
);
|
||||
|
||||
// Second shape (left side)
|
||||
const leftShape = shapes[1].closest("div");
|
||||
expect(leftShape).toHaveClass(
|
||||
"hidden",
|
||||
"md:block",
|
||||
"absolute",
|
||||
"top-1/2",
|
||||
"left-0",
|
||||
"pointer-events-none",
|
||||
"z-10",
|
||||
);
|
||||
});
|
||||
|
||||
it("handles malformed post data gracefully", async () => {
|
||||
const malformedPost = {
|
||||
slug: "malformed",
|
||||
frontmatter: {
|
||||
title: "Malformed Post",
|
||||
description: "A malformed post for testing",
|
||||
author: "Test Author",
|
||||
date: "2025-01-15",
|
||||
},
|
||||
htmlContent: "<p>Content</p>",
|
||||
};
|
||||
|
||||
const { getBlogPostBySlug } = await import("../../lib/content");
|
||||
vi.mocked(getBlogPostBySlug).mockReturnValue(malformedPost);
|
||||
|
||||
const BlogPostPageComponent = await BlogPostPage({
|
||||
params: { slug: "malformed" },
|
||||
});
|
||||
render(BlogPostPageComponent);
|
||||
|
||||
expect(screen.getByText("Malformed Post")).toBeInTheDocument();
|
||||
expect(screen.getByText("Content")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,160 +0,0 @@
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import Button from "../../app/components/Button";
|
||||
|
||||
describe("Button Component", () => {
|
||||
it("renders button with default props", () => {
|
||||
render(<Button>Click me</Button>);
|
||||
|
||||
const button = screen.getByRole("button", { name: /click me/i });
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveClass("bg-[var(--color-surface-inverse-primary)]");
|
||||
expect(button).toHaveAttribute("type", "button");
|
||||
});
|
||||
|
||||
it("renders with custom className", () => {
|
||||
const customClass = "custom-button-class";
|
||||
render(<Button className={customClass}>Custom Button</Button>);
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
expect(button).toHaveClass(customClass);
|
||||
});
|
||||
|
||||
it("applies variant classes correctly", () => {
|
||||
const { rerender } = render(<Button variant="secondary">Secondary</Button>);
|
||||
let button = screen.getByRole("button");
|
||||
expect(button).toHaveClass("bg-transparent");
|
||||
|
||||
rerender(<Button variant="primary">Primary</Button>);
|
||||
button = screen.getByRole("button");
|
||||
expect(button).toHaveClass("bg-[var(--color-surface-default-primary)]");
|
||||
|
||||
rerender(<Button variant="outlined">Outlined</Button>);
|
||||
button = screen.getByRole("button");
|
||||
expect(button).toHaveClass("bg-transparent", "border-[1.5px]");
|
||||
});
|
||||
|
||||
it("applies size classes correctly", () => {
|
||||
const { rerender } = render(<Button size="small">Small</Button>);
|
||||
let button = screen.getByRole("button");
|
||||
expect(button).toHaveClass("px-[var(--spacing-measures-spacing-008)]");
|
||||
|
||||
rerender(<Button size="large">Large</Button>);
|
||||
button = screen.getByRole("button");
|
||||
expect(button).toHaveClass("px-[var(--spacing-scale-012)]");
|
||||
|
||||
rerender(<Button size="xlarge">XLarge</Button>);
|
||||
button = screen.getByRole("button");
|
||||
expect(button).toHaveClass("px-[var(--spacing-scale-020)]");
|
||||
});
|
||||
|
||||
it("renders as link when href is provided", () => {
|
||||
const href = "/test-page";
|
||||
render(<Button href={href}>Link Button</Button>);
|
||||
|
||||
const link = screen.getByRole("link", { name: /link button/i });
|
||||
expect(link).toBeInTheDocument();
|
||||
expect(link).toHaveAttribute("href", href);
|
||||
});
|
||||
|
||||
it("renders as button when href is not provided", () => {
|
||||
render(<Button>Regular Button</Button>);
|
||||
|
||||
expect(screen.queryByRole("link")).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("button")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles click events", () => {
|
||||
const handleClick = vi.fn();
|
||||
render(<Button onClick={handleClick}>Clickable</Button>);
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("applies disabled state correctly", () => {
|
||||
render(<Button disabled>Disabled Button</Button>);
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
expect(button).toBeDisabled();
|
||||
expect(button).toHaveClass(
|
||||
"disabled:opacity-50",
|
||||
"disabled:cursor-not-allowed",
|
||||
);
|
||||
expect(button).toHaveAttribute("aria-disabled", "true");
|
||||
expect(button).toHaveAttribute("tabIndex", "-1");
|
||||
});
|
||||
|
||||
it("applies proper accessibility attributes", () => {
|
||||
render(<Button ariaLabel="Custom label">Button</Button>);
|
||||
|
||||
const button = screen.getByRole("button", { name: /custom label/i });
|
||||
expect(button).toHaveAttribute("aria-label", "Custom label");
|
||||
});
|
||||
|
||||
it("applies hover effects correctly", () => {
|
||||
render(<Button>Hover Button</Button>);
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
expect(button).toHaveClass("hover:scale-[1.02]", "transition-all");
|
||||
});
|
||||
|
||||
it("applies focus styles correctly", () => {
|
||||
render(<Button>Focus Button</Button>);
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
expect(button).toHaveClass("focus:outline-none", "focus:ring-1");
|
||||
});
|
||||
|
||||
it("applies active styles correctly", () => {
|
||||
render(<Button>Active Button</Button>);
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
expect(button).toHaveClass("active:scale-[0.98]");
|
||||
});
|
||||
|
||||
it("handles target and rel props for links", () => {
|
||||
render(
|
||||
<Button href="/test" target="_blank" rel="noopener">
|
||||
External Link
|
||||
</Button>,
|
||||
);
|
||||
|
||||
const link = screen.getByRole("link");
|
||||
expect(link).toHaveAttribute("target", "_blank");
|
||||
expect(link).toHaveAttribute("rel", "noopener");
|
||||
});
|
||||
|
||||
it("forwards additional props", () => {
|
||||
render(<Button data-testid="test-button">Test Button</Button>);
|
||||
|
||||
const button = screen.getByTestId("test-button");
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies proper font styles for different sizes", () => {
|
||||
const { rerender } = render(<Button size="xsmall">XSmall</Button>);
|
||||
let button = screen.getByRole("button");
|
||||
expect(button).toHaveClass("text-[10px]", "leading-[12px]");
|
||||
|
||||
rerender(<Button size="xlarge">XLarge</Button>);
|
||||
button = screen.getByRole("button");
|
||||
expect(button).toHaveClass("text-[24px]", "leading-[28px]");
|
||||
});
|
||||
|
||||
it("applies proper border radius", () => {
|
||||
render(<Button>Rounded Button</Button>);
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
expect(button).toHaveClass("rounded-[var(--radius-measures-radius-full)]");
|
||||
});
|
||||
|
||||
it("maintains proper tab index when not disabled", () => {
|
||||
render(<Button>Tab Button</Button>);
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
expect(button).toHaveAttribute("tabIndex", "0");
|
||||
});
|
||||
});
|
||||
@@ -1,166 +0,0 @@
|
||||
import React from "react";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { expect, test, describe, vi } from "vitest";
|
||||
import Checkbox from "../../app/components/Checkbox";
|
||||
|
||||
describe("Checkbox Component", () => {
|
||||
test("renders with default props", () => {
|
||||
render(<Checkbox />);
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
expect(checkbox).toBeInTheDocument();
|
||||
expect(checkbox).toHaveAttribute("aria-checked", "false");
|
||||
});
|
||||
|
||||
test("renders with label", () => {
|
||||
render(<Checkbox label="Test checkbox" />);
|
||||
expect(screen.getByText("Test checkbox")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders as checked when checked prop is true", () => {
|
||||
render(<Checkbox checked={true} />);
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
expect(checkbox).toHaveAttribute("aria-checked", "true");
|
||||
});
|
||||
|
||||
test("renders as unchecked when checked prop is false", () => {
|
||||
render(<Checkbox checked={false} />);
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
expect(checkbox).toHaveAttribute("aria-checked", "false");
|
||||
});
|
||||
|
||||
test("calls onChange when clicked", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<Checkbox onChange={handleChange} />);
|
||||
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
expect(handleChange).toHaveBeenCalledWith({
|
||||
checked: true,
|
||||
value: undefined,
|
||||
event: expect.any(Object),
|
||||
});
|
||||
});
|
||||
|
||||
test("calls onChange when toggled from checked to unchecked", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<Checkbox checked={true} onChange={handleChange} />);
|
||||
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
expect(handleChange).toHaveBeenCalledWith({
|
||||
checked: false,
|
||||
value: undefined,
|
||||
event: expect.any(Object),
|
||||
});
|
||||
});
|
||||
|
||||
test("handles keyboard navigation", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<Checkbox onChange={handleChange} />);
|
||||
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
|
||||
// Test Space key
|
||||
fireEvent.keyDown(checkbox, { key: " " });
|
||||
expect(handleChange).toHaveBeenCalledWith({
|
||||
checked: true,
|
||||
value: undefined,
|
||||
event: expect.any(Object),
|
||||
});
|
||||
|
||||
// Test Enter key
|
||||
fireEvent.keyDown(checkbox, { key: "Enter" });
|
||||
expect(handleChange).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test("does not call onChange when disabled", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<Checkbox disabled={true} onChange={handleChange} />);
|
||||
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
expect(handleChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("applies disabled attributes when disabled", () => {
|
||||
render(<Checkbox disabled={true} />);
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
expect(checkbox).toHaveAttribute("aria-disabled", "true");
|
||||
expect(checkbox).toHaveAttribute("tabIndex", "-1");
|
||||
});
|
||||
|
||||
test("applies correct tabIndex when not disabled", () => {
|
||||
render(<Checkbox />);
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
expect(checkbox).toHaveAttribute("tabIndex", "0");
|
||||
});
|
||||
|
||||
test("renders with standard mode by default", () => {
|
||||
render(<Checkbox />);
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
expect(checkbox).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with inverse mode", () => {
|
||||
render(<Checkbox mode="inverse" />);
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
expect(checkbox).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("applies custom className", () => {
|
||||
render(<Checkbox className="custom-class" />);
|
||||
const label = screen.getByRole("checkbox").closest("label");
|
||||
expect(label).toHaveClass("custom-class");
|
||||
});
|
||||
|
||||
test("passes through additional props", () => {
|
||||
render(<Checkbox id="test-checkbox" name="test" value="test-value" />);
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
expect(checkbox).toHaveAttribute("id", "test-checkbox");
|
||||
});
|
||||
|
||||
test("renders hidden native input for form compatibility", () => {
|
||||
render(<Checkbox name="test" value="test-value" checked={true} />);
|
||||
const hiddenInput = screen.getByDisplayValue("test-value");
|
||||
expect(hiddenInput).toBeInTheDocument();
|
||||
expect(hiddenInput).toHaveAttribute("type", "checkbox");
|
||||
expect(hiddenInput).toHaveAttribute("name", "test");
|
||||
expect(hiddenInput).toBeChecked();
|
||||
});
|
||||
|
||||
test("applies aria-label when provided", () => {
|
||||
render(<Checkbox ariaLabel="Custom label" />);
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
expect(checkbox).toHaveAttribute("aria-label", "Custom label");
|
||||
});
|
||||
|
||||
test("prevents default on mouse down", () => {
|
||||
render(<Checkbox />);
|
||||
const label = screen.getByRole("checkbox").closest("label");
|
||||
const mouseDownEvent = new MouseEvent("mousedown", { bubbles: true });
|
||||
const preventDefaultSpy = vi.spyOn(mouseDownEvent, "preventDefault");
|
||||
|
||||
fireEvent(label, mouseDownEvent);
|
||||
expect(preventDefaultSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("renders checkmark SVG when checked", () => {
|
||||
render(<Checkbox checked={true} />);
|
||||
const svg = screen.getByRole("checkbox").querySelector("svg");
|
||||
expect(svg).toBeInTheDocument();
|
||||
expect(svg).toHaveAttribute("aria-hidden", "true");
|
||||
expect(svg).toHaveAttribute("focusable", "false");
|
||||
});
|
||||
|
||||
test("does not render checkmark SVG when unchecked", () => {
|
||||
render(<Checkbox checked={false} />);
|
||||
const svg = screen.getByRole("checkbox").querySelector("svg");
|
||||
expect(svg).toBeInTheDocument();
|
||||
// SVG should be present but checkmark should be transparent
|
||||
const path = svg.querySelector("polyline");
|
||||
expect(path).toHaveAttribute("stroke", "transparent");
|
||||
});
|
||||
});
|
||||
@@ -1,287 +0,0 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import ContentBanner from "../../app/components/ContentBanner";
|
||||
|
||||
// Mock Next.js components
|
||||
vi.mock("next/link", () => {
|
||||
return {
|
||||
default: ({ children, href, ...props }) => (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock asset utils
|
||||
vi.mock("../../lib/assetUtils", () => ({
|
||||
getAssetPath: vi.fn((asset) => `/assets/${asset}`),
|
||||
ASSETS: {
|
||||
CONTENT_BANNER_1: "Content_Banner_1.svg",
|
||||
CONTENT_BANNER_2: "Content_Banner_2.svg",
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock blog post data
|
||||
const mockPost = {
|
||||
slug: "test-article",
|
||||
frontmatter: {
|
||||
title: "Test Article Title",
|
||||
description: "This is a test article description",
|
||||
author: "Test Author",
|
||||
date: "2025-04-15",
|
||||
thumbnail: {
|
||||
horizontal: "test-article-horizontal.svg",
|
||||
},
|
||||
banner: {
|
||||
horizontal: "test-article-banner.svg",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe("ContentBanner", () => {
|
||||
it("renders the banner with correct structure", () => {
|
||||
render(<ContentBanner post={mockPost} />);
|
||||
|
||||
// Check that the banner container exists - it's the first div with the specific classes
|
||||
const banner = document.querySelector(
|
||||
"div[class*='pt-[var(--measures-spacing-016)]']",
|
||||
);
|
||||
expect(banner).toBeInTheDocument();
|
||||
expect(banner).toHaveClass(
|
||||
"pt-[var(--measures-spacing-016)]",
|
||||
"md:pt-[var(--measures-spacing-008)]",
|
||||
"lg:pt-[50px]",
|
||||
"xl:pt-[112px]",
|
||||
"h-[275px]",
|
||||
"sm:h-[326px]",
|
||||
"md:h-[224px]",
|
||||
"lg:h-[358.4px]",
|
||||
"xl:h-[504px]",
|
||||
"relative",
|
||||
"w-full",
|
||||
"sm:overflow-hidden",
|
||||
);
|
||||
});
|
||||
|
||||
it("displays the background image correctly", () => {
|
||||
render(<ContentBanner post={mockPost} />);
|
||||
|
||||
// Check for background div with correct styling
|
||||
const backgroundDiv = document.querySelector(
|
||||
"div[style*='background-image']",
|
||||
);
|
||||
expect(backgroundDiv).toBeInTheDocument();
|
||||
expect(backgroundDiv).toHaveClass(
|
||||
"absolute",
|
||||
"inset-0",
|
||||
"w-full",
|
||||
"h-full",
|
||||
"bg-cover",
|
||||
"bg-no-repeat",
|
||||
"aspect-[320/225.5]",
|
||||
);
|
||||
});
|
||||
|
||||
it("shows banner image at md breakpoint and above", () => {
|
||||
render(<ContentBanner post={mockPost} />);
|
||||
|
||||
// Check for the md+ background div with banner image
|
||||
const mdBackgroundDiv = document.querySelector(
|
||||
"div[style*='test-article-banner.svg']",
|
||||
);
|
||||
expect(mdBackgroundDiv).toBeInTheDocument();
|
||||
expect(mdBackgroundDiv).toHaveClass("hidden", "md:block");
|
||||
});
|
||||
|
||||
it("displays the article title", () => {
|
||||
render(<ContentBanner post={mockPost} />);
|
||||
|
||||
expect(screen.getByText("Test Article Title")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays the article description", () => {
|
||||
render(<ContentBanner post={mockPost} />);
|
||||
|
||||
expect(
|
||||
screen.getByText("This is a test article description"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays the author and date metadata", () => {
|
||||
render(<ContentBanner post={mockPost} />);
|
||||
|
||||
expect(screen.getByText("Test Author")).toBeInTheDocument();
|
||||
expect(screen.getByText("April 2025")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies correct styling classes", () => {
|
||||
render(<ContentBanner post={mockPost} />);
|
||||
|
||||
// Check the content container div
|
||||
const contentContainer = document.querySelector(
|
||||
"div[class*='relative z-10']",
|
||||
);
|
||||
expect(contentContainer).toBeInTheDocument();
|
||||
expect(contentContainer).toHaveClass(
|
||||
"relative",
|
||||
"z-10",
|
||||
"h-full",
|
||||
"flex",
|
||||
"flex-col",
|
||||
);
|
||||
});
|
||||
|
||||
it("applies correct text styling", () => {
|
||||
render(<ContentBanner post={mockPost} />);
|
||||
|
||||
const title = screen.getByText("Test Article Title");
|
||||
expect(title).toHaveClass(
|
||||
"font-bricolage",
|
||||
"font-medium",
|
||||
"text-[18px]",
|
||||
"leading-[120%]",
|
||||
"text-[var(--color-content-inverse-brand-royal)]",
|
||||
);
|
||||
|
||||
const description = screen.getByText("This is a test article description");
|
||||
expect(description).toHaveClass(
|
||||
"font-inter",
|
||||
"font-normal",
|
||||
"text-[12px]",
|
||||
"leading-[16px]",
|
||||
"text-[var(--color-content-inverse-brand-royal)]",
|
||||
);
|
||||
});
|
||||
|
||||
it("applies correct metadata styling", () => {
|
||||
render(<ContentBanner post={mockPost} />);
|
||||
|
||||
const author = screen.getByText("Test Author");
|
||||
expect(author).toHaveClass(
|
||||
"font-inter",
|
||||
"font-normal",
|
||||
"text-[10px]",
|
||||
"leading-[14px]",
|
||||
"text-[var(--color-content-inverse-brand-royal)]",
|
||||
);
|
||||
|
||||
const date = screen.getByText("April 2025");
|
||||
expect(date).toHaveClass(
|
||||
"font-inter",
|
||||
"font-normal",
|
||||
"text-[10px]",
|
||||
"leading-[14px]",
|
||||
"text-[var(--color-content-inverse-brand-royal)]",
|
||||
);
|
||||
});
|
||||
|
||||
it("has proper spacing between elements", () => {
|
||||
render(<ContentBanner post={mockPost} />);
|
||||
|
||||
// Check the ContentContainer spacing
|
||||
const contentContainer = document.querySelector(
|
||||
"div[class*='relative z-20']",
|
||||
);
|
||||
expect(contentContainer).toHaveClass("gap-[var(--measures-spacing-012)]");
|
||||
});
|
||||
|
||||
it("has proper outer container padding", () => {
|
||||
render(<ContentBanner post={mockPost} />);
|
||||
|
||||
const outerContainer = document.querySelector(
|
||||
"div[class*='pt-[var(--measures-spacing-016)]']",
|
||||
);
|
||||
expect(outerContainer).toHaveClass(
|
||||
"pt-[var(--measures-spacing-016)]",
|
||||
"md:pt-[var(--measures-spacing-008)]",
|
||||
"lg:pt-[50px]",
|
||||
"xl:pt-[112px]",
|
||||
);
|
||||
});
|
||||
|
||||
it("handles missing post data gracefully", () => {
|
||||
const incompletePost = {
|
||||
slug: "incomplete",
|
||||
frontmatter: {
|
||||
title: "Incomplete Post",
|
||||
// Missing other fields
|
||||
},
|
||||
};
|
||||
|
||||
render(<ContentBanner post={incompletePost} />);
|
||||
|
||||
expect(screen.getByText("Incomplete Post")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("falls back to thumbnail.horizontal when banner.horizontal is missing", () => {
|
||||
const postWithoutBanner = {
|
||||
...mockPost,
|
||||
frontmatter: {
|
||||
...mockPost.frontmatter,
|
||||
banner: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
render(<ContentBanner post={postWithoutBanner} />);
|
||||
|
||||
// Should use thumbnail.horizontal for md+ breakpoint
|
||||
const mdBackgroundDiv = document.querySelector(
|
||||
"div[style*='test-article-horizontal.svg'][class*='md:block']",
|
||||
);
|
||||
expect(mdBackgroundDiv).toBeInTheDocument();
|
||||
expect(mdBackgroundDiv).toHaveClass("hidden", "md:block");
|
||||
});
|
||||
|
||||
it("falls back to default banner when no images are provided", () => {
|
||||
const postWithoutImages = {
|
||||
...mockPost,
|
||||
frontmatter: {
|
||||
...mockPost.frontmatter,
|
||||
thumbnail: undefined,
|
||||
banner: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
render(<ContentBanner post={postWithoutImages} />);
|
||||
|
||||
// Should use default banner for md+ breakpoint
|
||||
const mdBackgroundDiv = document.querySelector(
|
||||
"div[style*='Content_Banner_2.svg']",
|
||||
);
|
||||
expect(mdBackgroundDiv).toBeInTheDocument();
|
||||
expect(mdBackgroundDiv).toHaveClass("hidden", "md:block");
|
||||
});
|
||||
|
||||
it("applies responsive text sizing", () => {
|
||||
render(<ContentBanner post={mockPost} />);
|
||||
|
||||
const title = screen.getByText("Test Article Title");
|
||||
expect(title).toHaveClass(
|
||||
"sm:text-[24px]",
|
||||
"md:text-[32px]",
|
||||
"lg:text-[44px]",
|
||||
"xl:text-[64px]",
|
||||
);
|
||||
|
||||
const description = screen.getByText("This is a test article description");
|
||||
expect(description).toHaveClass(
|
||||
"sm:text-[14px]",
|
||||
"md:text-[14px]",
|
||||
"lg:text-[18px]",
|
||||
"xl:text-[24px]",
|
||||
);
|
||||
});
|
||||
|
||||
it("has proper accessibility attributes", () => {
|
||||
render(<ContentBanner post={mockPost} />);
|
||||
|
||||
// Check that the component renders without accessibility errors
|
||||
const banner = document.querySelector("div");
|
||||
expect(banner).toBeInTheDocument();
|
||||
|
||||
// Check that the icon has proper alt text
|
||||
const icon = screen.getByAltText("Icon for Test Article Title");
|
||||
expect(icon).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,321 +0,0 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { expect, describe, it, vi, beforeEach } from "vitest";
|
||||
import { axe, toHaveNoViolations } from "jest-axe";
|
||||
import ContextMenu from "../../app/components/ContextMenu";
|
||||
import ContextMenuItem from "../../app/components/ContextMenuItem";
|
||||
import ContextMenuSection from "../../app/components/ContextMenuSection";
|
||||
import ContextMenuDivider from "../../app/components/ContextMenuDivider";
|
||||
|
||||
expect.extend(toHaveNoViolations);
|
||||
|
||||
describe("ContextMenu Component", () => {
|
||||
const defaultProps = {
|
||||
children: "Context Menu Content",
|
||||
};
|
||||
|
||||
describe("Rendering", () => {
|
||||
it("renders with default props", () => {
|
||||
render(<ContextMenu {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText("Context Menu Content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders with custom className", () => {
|
||||
render(<ContextMenu {...defaultProps} className="custom-class" />);
|
||||
|
||||
const menu = screen.getByText("Context Menu Content").closest("div");
|
||||
expect(menu).toHaveClass("custom-class");
|
||||
});
|
||||
|
||||
it("applies correct base styles", () => {
|
||||
render(<ContextMenu {...defaultProps} />);
|
||||
|
||||
const menu = screen.getByText("Context Menu Content").closest("div");
|
||||
expect(menu).toHaveClass(
|
||||
"bg-black",
|
||||
"border",
|
||||
"rounded-[var(--measures-radius-medium)]",
|
||||
"shadow-lg",
|
||||
"p-[4px]",
|
||||
);
|
||||
});
|
||||
|
||||
it("has solid black background", () => {
|
||||
render(<ContextMenu {...defaultProps} />);
|
||||
|
||||
const menu = screen.getByText("Context Menu Content").closest("div");
|
||||
expect(menu).toHaveStyle({ backgroundColor: "#000000" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Accessibility", () => {
|
||||
it("has no accessibility violations", async () => {
|
||||
const { container } = render(
|
||||
<ContextMenu {...defaultProps}>
|
||||
<ContextMenuItem onClick={vi.fn()}>Menu Item</ContextMenuItem>
|
||||
</ContextMenu>,
|
||||
);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("has proper role", () => {
|
||||
render(<ContextMenu {...defaultProps} />);
|
||||
|
||||
const menu = screen.getByText("Context Menu Content").closest("div");
|
||||
expect(menu).toHaveAttribute("role", "menu");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("ContextMenuItem Component", () => {
|
||||
const defaultProps = {
|
||||
children: "Menu Item",
|
||||
onClick: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("Rendering", () => {
|
||||
it("renders with default props", () => {
|
||||
render(<ContextMenuItem {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText("Menu Item")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders as selected when selected prop is true", () => {
|
||||
render(<ContextMenuItem {...defaultProps} selected={true} />);
|
||||
|
||||
const item = screen.getByRole("menuitem");
|
||||
expect(item).toHaveClass(
|
||||
"bg-[var(--color-surface-default-secondary)]",
|
||||
"rounded-[var(--measures-radius-small)]",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders with submenu arrow when hasSubmenu prop is true", () => {
|
||||
render(<ContextMenuItem {...defaultProps} hasSubmenu={true} />);
|
||||
|
||||
// Check for the right-pointing chevron SVG
|
||||
const item = screen.getByRole("menuitem");
|
||||
const svg = item.querySelector("svg:last-child");
|
||||
expect(svg).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders with checkmark when selected prop is true", () => {
|
||||
render(<ContextMenuItem {...defaultProps} selected={true} />);
|
||||
|
||||
// Check for the checkmark SVG
|
||||
const item = screen.getByRole("menuitem");
|
||||
const svg = item.querySelector("svg:first-child");
|
||||
expect(svg).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies correct size styles", () => {
|
||||
render(<ContextMenuItem {...defaultProps} size="small" />);
|
||||
|
||||
const item = screen.getByRole("menuitem");
|
||||
expect(item).toHaveClass("text-[10px]", "leading-[14px]");
|
||||
});
|
||||
|
||||
it("applies medium size styles", () => {
|
||||
render(<ContextMenuItem {...defaultProps} size="medium" />);
|
||||
|
||||
const item = screen.getByRole("menuitem");
|
||||
expect(item).toHaveClass("text-[14px]", "leading-[20px]");
|
||||
});
|
||||
|
||||
it("applies large size styles", () => {
|
||||
render(<ContextMenuItem {...defaultProps} size="large" />);
|
||||
|
||||
const item = screen.getByRole("menuitem");
|
||||
expect(item).toHaveClass("text-[16px]", "leading-[24px]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Interaction", () => {
|
||||
it("calls onClick when clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ContextMenuItem {...defaultProps} />);
|
||||
|
||||
const item = screen.getByText("Menu Item");
|
||||
await user.click(item);
|
||||
|
||||
expect(defaultProps.onClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not call onClick when disabled", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ContextMenuItem {...defaultProps} disabled={true} />);
|
||||
|
||||
const item = screen.getByText("Menu Item");
|
||||
await user.click(item);
|
||||
|
||||
expect(defaultProps.onClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("has hover effects", () => {
|
||||
render(<ContextMenuItem {...defaultProps} />);
|
||||
|
||||
const item = screen.getByRole("menuitem");
|
||||
expect(item).toHaveClass(
|
||||
"hover:!bg-[var(--color-surface-default-secondary)]",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Accessibility", () => {
|
||||
it("has no accessibility violations", async () => {
|
||||
const { container } = render(
|
||||
<ContextMenu>
|
||||
<ContextMenuItem {...defaultProps} />
|
||||
</ContextMenu>,
|
||||
);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("has proper role", () => {
|
||||
render(<ContextMenuItem {...defaultProps} />);
|
||||
|
||||
const item = screen.getByRole("menuitem");
|
||||
expect(item).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Styling", () => {
|
||||
it("applies correct text color", () => {
|
||||
render(<ContextMenuItem {...defaultProps} />);
|
||||
|
||||
const item = screen.getByRole("menuitem");
|
||||
expect(item).toHaveClass(
|
||||
"text-[var(--color-content-default-brand-primary)]",
|
||||
);
|
||||
});
|
||||
|
||||
it("applies correct padding", () => {
|
||||
render(<ContextMenuItem {...defaultProps} />);
|
||||
|
||||
const item = screen.getByRole("menuitem");
|
||||
expect(item).toHaveClass("px-[8px]", "py-[4px]");
|
||||
});
|
||||
|
||||
it("applies correct gap between checkmark and text", () => {
|
||||
render(<ContextMenuItem {...defaultProps} selected={true} />);
|
||||
|
||||
const item = screen.getByText("Menu Item").closest("div");
|
||||
expect(item).toHaveClass("gap-[8px]");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("ContextMenuSection Component", () => {
|
||||
const defaultProps = {
|
||||
title: "Section Title",
|
||||
children: "Section Content",
|
||||
};
|
||||
|
||||
describe("Rendering", () => {
|
||||
it("renders with title and children", () => {
|
||||
render(<ContextMenuSection {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText("Section Title")).toBeInTheDocument();
|
||||
expect(screen.getByText("Section Content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders without title when not provided", () => {
|
||||
render(<ContextMenuSection>Section Content</ContextMenuSection>);
|
||||
|
||||
expect(screen.getByText("Section Content")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Section Title")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies correct title styling", () => {
|
||||
render(<ContextMenuSection {...defaultProps} />);
|
||||
|
||||
const title = screen.getByText("Section Title");
|
||||
expect(title).toHaveClass(
|
||||
"text-[var(--color-content-default-primary)]",
|
||||
"font-medium",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Accessibility", () => {
|
||||
it("has no accessibility violations", async () => {
|
||||
const { container } = render(<ContextMenuSection {...defaultProps} />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("ContextMenuDivider Component", () => {
|
||||
describe("Rendering", () => {
|
||||
it("renders divider", () => {
|
||||
render(<ContextMenuDivider />);
|
||||
|
||||
const divider = screen.getByRole("separator");
|
||||
expect(divider).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies correct styling", () => {
|
||||
render(<ContextMenuDivider />);
|
||||
|
||||
const divider = screen.getByRole("separator");
|
||||
expect(divider).toHaveClass(
|
||||
"border-t",
|
||||
"border-[var(--color-border-default-tertiary)]",
|
||||
"my-1",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Accessibility", () => {
|
||||
it("has no accessibility violations", async () => {
|
||||
const { container } = render(<ContextMenuDivider />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("ContextMenu Components Integration", () => {
|
||||
const TestMenu = () => (
|
||||
<ContextMenu>
|
||||
<ContextMenuSection title="First Section">
|
||||
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
|
||||
<ContextMenuItem onClick={vi.fn()} selected={true}>
|
||||
Item 2
|
||||
</ContextMenuItem>
|
||||
</ContextMenuSection>
|
||||
<ContextMenuDivider />
|
||||
<ContextMenuSection title="Second Section">
|
||||
<ContextMenuItem onClick={vi.fn()} hasSubmenu={true}>
|
||||
Item 3
|
||||
</ContextMenuItem>
|
||||
</ContextMenuSection>
|
||||
</ContextMenu>
|
||||
);
|
||||
|
||||
it("renders all components together", () => {
|
||||
render(<TestMenu />);
|
||||
|
||||
expect(screen.getByText("First Section")).toBeInTheDocument();
|
||||
expect(screen.getByText("Item 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Item 2")).toBeInTheDocument();
|
||||
expect(screen.getByText("Second Section")).toBeInTheDocument();
|
||||
expect(screen.getByText("Item 3")).toBeInTheDocument();
|
||||
expect(screen.getByRole("separator")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("has no accessibility violations when integrated", async () => {
|
||||
const { container } = render(<TestMenu />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
});
|
||||
@@ -1,144 +0,0 @@
|
||||
import { render, screen, cleanup } from "@testing-library/react";
|
||||
import { describe, test, expect, afterEach } from "vitest";
|
||||
import FeatureGrid from "../../app/components/FeatureGrid";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe("FeatureGrid Component", () => {
|
||||
test("renders with title and subtitle", () => {
|
||||
render(
|
||||
<FeatureGrid
|
||||
title="Feature Tools"
|
||||
subtitle="Everything you need to build better communities"
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<FeatureGrid title="Test" subtitle="Test" className="custom-class" />,
|
||||
);
|
||||
|
||||
const section = document.querySelector("section");
|
||||
expect(section).toHaveClass("custom-class");
|
||||
});
|
||||
|
||||
test("renders all four MiniCard components", () => {
|
||||
render(<FeatureGrid title="Test" subtitle="Test" />);
|
||||
|
||||
// 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(<FeatureGrid title="Feature Title" subtitle="Feature Subtitle" />);
|
||||
|
||||
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(<FeatureGrid title="Test" subtitle="Test" />);
|
||||
|
||||
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(<FeatureGrid title="Test" subtitle="Test" />);
|
||||
|
||||
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(<FeatureGrid title="Test" subtitle="Test" />);
|
||||
|
||||
const grid = screen.getByRole("grid");
|
||||
expect(grid).toHaveClass("grid", "grid-cols-2", "md:grid-cols-4");
|
||||
});
|
||||
|
||||
test("renders MiniCard with correct props", () => {
|
||||
render(<FeatureGrid title="Test" subtitle="Test" />);
|
||||
|
||||
// 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(<FeatureGrid title="Test" subtitle="Test" />);
|
||||
|
||||
const section = document.querySelector("section");
|
||||
expect(section).toBeInTheDocument();
|
||||
|
||||
const grid = screen.getByRole("grid");
|
||||
expect(grid).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles missing optional props gracefully", () => {
|
||||
render(<FeatureGrid />);
|
||||
|
||||
// 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(<FeatureGrid title="Test" subtitle="Test" />);
|
||||
|
||||
const container = document
|
||||
.querySelector("section")
|
||||
.querySelector('[class*="focus-within:ring-2"]');
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,285 +0,0 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import Footer from "../../app/components/Footer";
|
||||
|
||||
describe("Footer", () => {
|
||||
test("renders footer with correct structure", () => {
|
||||
render(<Footer />);
|
||||
|
||||
const footers = screen.getAllByRole("contentinfo");
|
||||
expect(footers.length).toBeGreaterThan(0);
|
||||
const footer = footers[0];
|
||||
expect(footer).toBeInTheDocument();
|
||||
expect(footer).toHaveClass("bg-[var(--color-surface-default-primary)]");
|
||||
expect(footer).toHaveClass("w-full");
|
||||
});
|
||||
|
||||
test("renders schema markup for organization information", () => {
|
||||
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");
|
||||
expect(schemaData.email).toBe("medlab@colorado.edu");
|
||||
expect(schemaData.url).toBe("https://communityrule.com");
|
||||
expect(schemaData.sameAs).toContain(
|
||||
"https://bsky.app/profile/medlabboulder",
|
||||
);
|
||||
expect(schemaData.sameAs).toContain("https://gitlab.com/medlabboulder");
|
||||
});
|
||||
|
||||
test("renders organization name and contact information", () => {
|
||||
render(<Footer />);
|
||||
|
||||
expect(
|
||||
screen.getAllByText("Media Economies Design Lab").length,
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
const emailLinks = screen.getAllByRole("link", {
|
||||
name: "medlab@colorado.edu",
|
||||
});
|
||||
expect(emailLinks.length).toBeGreaterThan(0);
|
||||
const emailLink = emailLinks[0];
|
||||
expect(emailLink).toBeInTheDocument();
|
||||
expect(emailLink).toHaveAttribute("href", "mailto:medlab@colorado.edu");
|
||||
});
|
||||
|
||||
test("renders social media links with correct accessibility", () => {
|
||||
render(<Footer />);
|
||||
|
||||
// Check Bluesky link
|
||||
const blueskyLinks = screen.getAllByRole("link", {
|
||||
name: "Follow us on Bluesky",
|
||||
});
|
||||
expect(blueskyLinks.length).toBeGreaterThan(0);
|
||||
const blueskyLink = blueskyLinks[0];
|
||||
expect(blueskyLink).toBeInTheDocument();
|
||||
expect(screen.getAllByText("medlabboulder").length).toBeGreaterThan(0);
|
||||
|
||||
// Check GitLab link
|
||||
const gitlabLinks = screen.getAllByRole("link", {
|
||||
name: "Follow us on GitLab",
|
||||
});
|
||||
expect(gitlabLinks.length).toBeGreaterThan(0);
|
||||
const gitlabLink = gitlabLinks[0];
|
||||
expect(gitlabLink).toBeInTheDocument();
|
||||
|
||||
// Check social media images
|
||||
const blueskyImages = screen.getAllByAltText("Bluesky");
|
||||
expect(blueskyImages.length).toBeGreaterThan(0);
|
||||
const blueskyImage = blueskyImages[0];
|
||||
expect(blueskyImage).toBeInTheDocument();
|
||||
expect(blueskyImage).toHaveAttribute("src", "/assets/Bluesky_Logo.svg");
|
||||
|
||||
const gitlabImages = screen.getAllByAltText("GitLab");
|
||||
expect(gitlabImages.length).toBeGreaterThan(0);
|
||||
const gitlabImage = gitlabImages[0];
|
||||
expect(gitlabImage).toBeInTheDocument();
|
||||
expect(gitlabImage).toHaveAttribute("src", "/assets/GitLab_Icon.png");
|
||||
});
|
||||
|
||||
test("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);
|
||||
});
|
||||
|
||||
test("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);
|
||||
expect(
|
||||
screen.getAllByRole("link", { name: "Cookies Settings" }).length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("renders copyright information", () => {
|
||||
render(<Footer />);
|
||||
|
||||
expect(screen.getAllByText("© All right reserved").length).toBeGreaterThan(
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
test("renders responsive logo configurations", () => {
|
||||
render(<Footer />);
|
||||
|
||||
// Check that logo containers exist for different breakpoints
|
||||
const logoContainers = document.querySelectorAll(
|
||||
'[class*="block sm:hidden"], [class*="hidden sm:block lg:hidden"], [class*="hidden lg:block"]',
|
||||
);
|
||||
expect(logoContainers.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("has correct CSS classes for responsive design", () => {
|
||||
render(<Footer />);
|
||||
|
||||
const footers = screen.getAllByRole("contentinfo");
|
||||
expect(footers.length).toBeGreaterThan(0);
|
||||
const footer = footers[0];
|
||||
const mainContainer = footer.querySelector("div");
|
||||
|
||||
expect(mainContainer).toHaveClass("flex");
|
||||
expect(mainContainer).toHaveClass("flex-col");
|
||||
expect(mainContainer).toHaveClass("items-start");
|
||||
expect(mainContainer).toHaveClass("mx-auto");
|
||||
});
|
||||
|
||||
test("renders separator component", () => {
|
||||
render(<Footer />);
|
||||
|
||||
// The Separator component should be rendered (it uses a div with border, not hr)
|
||||
const separator = document.querySelector(
|
||||
".bg-\\[var\\(--border-color-default-secondary\\)\\]",
|
||||
);
|
||||
expect(separator).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("social media links have hover and focus states", () => {
|
||||
render(<Footer />);
|
||||
|
||||
const blueskyLinks = screen.getAllByRole("link", {
|
||||
name: "Follow us on Bluesky",
|
||||
});
|
||||
expect(blueskyLinks.length).toBeGreaterThan(0);
|
||||
expect(blueskyLinks[0]).toHaveClass("hover:opacity-80");
|
||||
expect(blueskyLinks[0]).toHaveClass("active:opacity-60");
|
||||
expect(blueskyLinks[0]).toHaveClass("focus:opacity-80");
|
||||
expect(blueskyLinks[0]).toHaveClass("transition-opacity");
|
||||
|
||||
const gitlabLinks = screen.getAllByRole("link", {
|
||||
name: "Follow us on GitLab",
|
||||
});
|
||||
expect(gitlabLinks.length).toBeGreaterThan(0);
|
||||
expect(gitlabLinks[0]).toHaveClass("hover:opacity-80");
|
||||
expect(gitlabLinks[0]).toHaveClass("active:opacity-60");
|
||||
expect(gitlabLinks[0]).toHaveClass("focus:opacity-80");
|
||||
expect(gitlabLinks[0]).toHaveClass("transition-opacity");
|
||||
});
|
||||
|
||||
test("navigation links have hover and focus states", () => {
|
||||
render(<Footer />);
|
||||
|
||||
const useCasesLinks = screen.getAllByRole("link", { name: "Use cases" });
|
||||
expect(useCasesLinks.length).toBeGreaterThan(0);
|
||||
expect(useCasesLinks[0]).toHaveClass("hover:opacity-80");
|
||||
expect(useCasesLinks[0]).toHaveClass("active:opacity-60");
|
||||
expect(useCasesLinks[0]).toHaveClass("focus:opacity-80");
|
||||
expect(useCasesLinks[0]).toHaveClass("transition-opacity");
|
||||
});
|
||||
|
||||
test("legal links have hover and focus states", () => {
|
||||
render(<Footer />);
|
||||
|
||||
const privacyLinks = screen.getAllByRole("link", {
|
||||
name: "Privacy Policy",
|
||||
});
|
||||
expect(privacyLinks.length).toBeGreaterThan(0);
|
||||
expect(privacyLinks[0]).toHaveClass("hover:opacity-80");
|
||||
expect(privacyLinks[0]).toHaveClass("active:opacity-60");
|
||||
expect(privacyLinks[0]).toHaveClass("focus:opacity-80");
|
||||
expect(privacyLinks[0]).toHaveClass("transition-opacity");
|
||||
});
|
||||
|
||||
test("email link has hover and focus states", () => {
|
||||
render(<Footer />);
|
||||
|
||||
const emailLinks = screen.getAllByRole("link", {
|
||||
name: "medlab@colorado.edu",
|
||||
});
|
||||
expect(emailLinks.length).toBeGreaterThan(0);
|
||||
expect(emailLinks[0]).toHaveClass("hover:opacity-80");
|
||||
expect(emailLinks[0]).toHaveClass("active:opacity-60");
|
||||
expect(emailLinks[0]).toHaveClass("focus:opacity-80");
|
||||
expect(emailLinks[0]).toHaveClass("transition-opacity");
|
||||
});
|
||||
|
||||
test("social media images have hover effects", () => {
|
||||
render(<Footer />);
|
||||
|
||||
const blueskyImages = screen.getAllByAltText("Bluesky");
|
||||
expect(blueskyImages.length).toBeGreaterThan(0);
|
||||
expect(blueskyImages[0]).toHaveClass("group-hover:scale-110");
|
||||
expect(blueskyImages[0]).toHaveClass("transition-transform");
|
||||
|
||||
const gitlabImages = screen.getAllByAltText("GitLab");
|
||||
expect(gitlabImages.length).toBeGreaterThan(0);
|
||||
expect(gitlabImages[0]).toHaveClass("group-hover:scale-110");
|
||||
expect(gitlabImages[0]).toHaveClass("transition-transform");
|
||||
expect(gitlabImages[0]).toHaveClass("grayscale");
|
||||
});
|
||||
|
||||
test("renders multiple instances of navigation links for responsive design", () => {
|
||||
render(<Footer />);
|
||||
|
||||
// Should have navigation links in the footer
|
||||
const useCasesLinks = screen.getAllByText("Use cases");
|
||||
const learnLinks = screen.getAllByText("Learn");
|
||||
const aboutLinks = screen.getAllByText("About");
|
||||
|
||||
expect(useCasesLinks.length).toBeGreaterThan(0);
|
||||
expect(learnLinks.length).toBeGreaterThan(0);
|
||||
expect(aboutLinks.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("has proper focus management for accessibility", () => {
|
||||
render(<Footer />);
|
||||
|
||||
// Get specific links that should have focus management
|
||||
const emailLinks = screen.getAllByRole("link", {
|
||||
name: "medlab@colorado.edu",
|
||||
});
|
||||
const blueskyLinks = screen.getAllByRole("link", {
|
||||
name: "Follow us on Bluesky",
|
||||
});
|
||||
const gitlabLinks = screen.getAllByRole("link", {
|
||||
name: "Follow us on GitLab",
|
||||
});
|
||||
|
||||
// Use the first instance of each social media link
|
||||
const blueskyLink = blueskyLinks[0];
|
||||
const gitlabLink = gitlabLinks[0];
|
||||
|
||||
// Check email links (multiple due to responsive design)
|
||||
emailLinks.forEach((emailLink) => {
|
||||
expect(emailLink).toHaveClass("focus:outline-none");
|
||||
expect(emailLink).toHaveClass("focus:ring-2");
|
||||
expect(emailLink).toHaveClass("focus:ring-offset-2");
|
||||
expect(emailLink).toHaveClass(
|
||||
"focus:ring-[var(--color-content-default-primary)]",
|
||||
);
|
||||
expect(emailLink).toHaveClass(
|
||||
"focus:ring-offset-[var(--color-surface-default-primary)]",
|
||||
);
|
||||
});
|
||||
|
||||
// Check social media links
|
||||
[blueskyLink, gitlabLink].forEach((link) => {
|
||||
expect(link).toHaveClass("focus:outline-none");
|
||||
expect(link).toHaveClass("focus:ring-2");
|
||||
expect(link).toHaveClass("focus:ring-offset-2");
|
||||
expect(link).toHaveClass(
|
||||
"focus:ring-[var(--color-content-default-primary)]",
|
||||
);
|
||||
expect(link).toHaveClass(
|
||||
"focus:ring-offset-[var(--color-surface-default-primary)]",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,334 +0,0 @@
|
||||
import { describe, test, expect, beforeEach } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import Header, {
|
||||
navigationItems,
|
||||
avatarImages,
|
||||
logoConfig,
|
||||
} from "../../app/components/Header.js";
|
||||
|
||||
describe("Header", () => {
|
||||
beforeEach(() => {
|
||||
// Clear any existing rendered content
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
describe("Accessibility and Landmarks", () => {
|
||||
test("renders header with correct structure and accessibility attributes", () => {
|
||||
const { container } = render(<Header />);
|
||||
|
||||
// Check main header structure - use container to scope the search
|
||||
const header = container.querySelector(
|
||||
'[role="banner"][aria-label="Main navigation header"]',
|
||||
);
|
||||
expect(header).toBeInTheDocument();
|
||||
expect(header).toHaveAttribute("aria-label", "Main navigation header");
|
||||
|
||||
// Check navigation - use container to scope the search
|
||||
const nav = container.querySelector(
|
||||
'[role="navigation"][aria-label="Main navigation"]',
|
||||
);
|
||||
expect(nav).toBeInTheDocument();
|
||||
expect(nav).toHaveAttribute("aria-label", "Main navigation");
|
||||
});
|
||||
|
||||
test("renders all navigation items with proper accessibility", () => {
|
||||
render(<Header />);
|
||||
|
||||
// Check all navigation items have proper aria-labels - use menuitem role since they're in a menubar
|
||||
expect(
|
||||
screen.getAllByRole("menuitem", { name: "Navigate to Use cases page" })
|
||||
.length,
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
screen.getAllByRole("menuitem", { name: "Navigate to Learn page" })
|
||||
.length,
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
screen.getAllByRole("menuitem", { name: "Navigate to About page" })
|
||||
.length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Schema Markup", () => {
|
||||
test("renders schema markup for site navigation", () => {
|
||||
render(<Header />);
|
||||
|
||||
const script = document.querySelector(
|
||||
'script[type="application/ld+json"]',
|
||||
);
|
||||
expect(script).toBeInTheDocument();
|
||||
|
||||
const schemaData = JSON.parse(script.textContent);
|
||||
expect(schemaData["@type"]).toBe("WebSite");
|
||||
expect(schemaData.name).toBe("CommunityRule");
|
||||
expect(schemaData.url).toBe("https://communityrule.com");
|
||||
expect(schemaData.potentialAction["@type"]).toBe("SearchAction");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Configuration Data", () => {
|
||||
test("navigationItems has correct structure and count", () => {
|
||||
expect(navigationItems).toHaveLength(3);
|
||||
expect(navigationItems[0]).toEqual({
|
||||
href: "#",
|
||||
text: "Use cases",
|
||||
extraPadding: true,
|
||||
});
|
||||
expect(navigationItems[1]).toEqual({
|
||||
href: "/learn",
|
||||
text: "Learn",
|
||||
});
|
||||
expect(navigationItems[2]).toEqual({
|
||||
href: "#",
|
||||
text: "About",
|
||||
});
|
||||
});
|
||||
|
||||
test("avatarImages has correct structure and count", () => {
|
||||
expect(avatarImages).toHaveLength(3);
|
||||
expect(avatarImages[0]).toEqual({
|
||||
src: "/assets/Avatar_1.png",
|
||||
alt: "Avatar 1",
|
||||
});
|
||||
expect(avatarImages[1]).toEqual({
|
||||
src: "/assets/Avatar_2.png",
|
||||
alt: "Avatar 2",
|
||||
});
|
||||
expect(avatarImages[2]).toEqual({
|
||||
src: "/assets/Avatar_3.png",
|
||||
alt: "Avatar 3",
|
||||
});
|
||||
});
|
||||
|
||||
test("logoConfig has correct structure and count", () => {
|
||||
expect(logoConfig).toHaveLength(5);
|
||||
|
||||
// Check first config (xs)
|
||||
expect(logoConfig[0]).toEqual({
|
||||
breakpoint: "block sm:hidden",
|
||||
size: "header",
|
||||
showText: false,
|
||||
});
|
||||
|
||||
// Check last config (xl+)
|
||||
expect(logoConfig[4]).toEqual({
|
||||
breakpoint: "hidden xl:block",
|
||||
size: "headerXl",
|
||||
showText: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Logo Configuration", () => {
|
||||
test("renders correct number of logo variants", () => {
|
||||
render(<Header />);
|
||||
|
||||
const logoWrappers = screen.getAllByTestId("logo-wrapper");
|
||||
expect(logoWrappers).toHaveLength(logoConfig.length);
|
||||
});
|
||||
|
||||
test("logo wrappers include expected breakpoint classes", () => {
|
||||
render(<Header />);
|
||||
|
||||
const logoWrappers = screen.getAllByTestId("logo-wrapper");
|
||||
|
||||
// Check first logo variant (xs only)
|
||||
expect(logoWrappers[0]).toHaveClass("block", "sm:hidden");
|
||||
|
||||
// Check second logo variant (sm only)
|
||||
expect(logoWrappers[1]).toHaveClass("hidden", "sm:block", "md:hidden");
|
||||
|
||||
// Check third logo variant (md only)
|
||||
expect(logoWrappers[2]).toHaveClass("hidden", "md:block", "lg:hidden");
|
||||
|
||||
// Check fourth logo variant (lg only)
|
||||
expect(logoWrappers[3]).toHaveClass("hidden", "lg:block", "xl:hidden");
|
||||
|
||||
// Check fifth logo variant (xl+)
|
||||
expect(logoWrappers[4]).toHaveClass("hidden", "xl:block");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Navigation Structure", () => {
|
||||
test("renders all breakpoint-specific navigation containers", () => {
|
||||
render(<Header />);
|
||||
|
||||
expect(screen.getByTestId("nav-xs")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("nav-sm")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("nav-md")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("nav-lg")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("nav-xl")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("navigation containers use expected breakpoint classes", () => {
|
||||
render(<Header />);
|
||||
|
||||
// XSmall navigation
|
||||
const navXs = screen.getByTestId("nav-xs");
|
||||
expect(navXs).toHaveClass("block", "sm:hidden");
|
||||
|
||||
// Small navigation
|
||||
const navSm = screen.getByTestId("nav-sm");
|
||||
expect(navSm).toHaveClass("hidden", "sm:block", "md:hidden");
|
||||
|
||||
// Medium navigation
|
||||
const navMd = screen.getByTestId("nav-md");
|
||||
expect(navMd).toHaveClass("hidden", "md:block", "lg:hidden");
|
||||
|
||||
// Large navigation
|
||||
const navLg = screen.getByTestId("nav-lg");
|
||||
expect(navLg).toHaveClass("hidden", "lg:block", "xl:hidden");
|
||||
|
||||
// XLarge navigation
|
||||
const navXl = screen.getByTestId("nav-xl");
|
||||
expect(navXl).toHaveClass("hidden", "xl:block");
|
||||
});
|
||||
|
||||
test("renders navigation items with correct text and links", () => {
|
||||
render(<Header />);
|
||||
|
||||
// Check navigation items
|
||||
expect(screen.getAllByText("Use cases").length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText("Learn").length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText("About").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("renders multiple instances of navigation items for responsive design", () => {
|
||||
render(<Header />);
|
||||
|
||||
// Should have multiple instances of navigation items for different breakpoints
|
||||
const useCasesLinks = screen.getAllByText("Use cases");
|
||||
const learnLinks = screen.getAllByText("Learn");
|
||||
const aboutLinks = screen.getAllByText("About");
|
||||
|
||||
expect(useCasesLinks.length).toBeGreaterThan(1);
|
||||
expect(learnLinks.length).toBeGreaterThan(1);
|
||||
expect(aboutLinks.length).toBeGreaterThan(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Authentication Structure", () => {
|
||||
test("renders all breakpoint-specific auth containers", () => {
|
||||
render(<Header />);
|
||||
|
||||
expect(screen.getByTestId("auth-xs")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("auth-sm")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("auth-md")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("auth-lg")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("auth-xl")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("auth containers use expected breakpoint classes", () => {
|
||||
render(<Header />);
|
||||
|
||||
// XSmall auth
|
||||
const authXs = screen.getByTestId("auth-xs");
|
||||
expect(authXs).toHaveClass("block", "sm:hidden");
|
||||
|
||||
// Small auth
|
||||
const authSm = screen.getByTestId("auth-sm");
|
||||
expect(authSm).toHaveClass("hidden", "sm:block", "md:hidden");
|
||||
|
||||
// Medium auth
|
||||
const authMd = screen.getByTestId("auth-md");
|
||||
expect(authMd).toHaveClass("hidden", "md:block", "lg:hidden");
|
||||
|
||||
// Large auth
|
||||
const authLg = screen.getByTestId("auth-lg");
|
||||
expect(authLg).toHaveClass("hidden", "lg:block", "xl:hidden");
|
||||
|
||||
// XLarge auth
|
||||
const authXl = screen.getByTestId("auth-xl");
|
||||
expect(authXl).toHaveClass("hidden", "xl:block");
|
||||
});
|
||||
|
||||
test("renders login button with correct accessibility", () => {
|
||||
render(<Header />);
|
||||
|
||||
const loginLinks = screen.getAllByRole("menuitem", {
|
||||
name: "Log in to your account",
|
||||
});
|
||||
expect(loginLinks.length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText("Log in").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("renders multiple login buttons for responsive design", () => {
|
||||
render(<Header />);
|
||||
|
||||
// Should have multiple login buttons for different breakpoints
|
||||
const loginButtons = screen.getAllByText("Log in");
|
||||
expect(loginButtons.length).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
test("renders create rule button with avatar decoration", () => {
|
||||
render(<Header />);
|
||||
|
||||
const createRuleButtons = screen.getAllByRole("button", {
|
||||
name: "Create a new rule with avatar decoration",
|
||||
});
|
||||
expect(createRuleButtons.length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText("Create rule").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("renders multiple create rule buttons for responsive design", () => {
|
||||
render(<Header />);
|
||||
|
||||
// Should have multiple create rule buttons for different breakpoints
|
||||
const createRuleButtons = screen.getAllByText("Create rule");
|
||||
expect(createRuleButtons.length).toBeGreaterThan(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Avatar Images", () => {
|
||||
test("renders avatar images with correct attributes", () => {
|
||||
render(<Header />);
|
||||
|
||||
const avatars = screen.getAllByRole("img");
|
||||
expect(avatars.length).toBeGreaterThan(0);
|
||||
|
||||
// Check for avatar images
|
||||
const avatarImages = avatars.filter(
|
||||
(img) =>
|
||||
img.alt === "Avatar 1" ||
|
||||
img.alt === "Avatar 2" ||
|
||||
img.alt === "Avatar 3",
|
||||
);
|
||||
expect(avatarImages.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sticky Header Behavior", () => {
|
||||
test("applies sticky positioning classes", () => {
|
||||
const { container } = render(<Header />);
|
||||
|
||||
const header = container.querySelector(
|
||||
'[role="banner"][aria-label="Main navigation header"]',
|
||||
);
|
||||
expect(header).toHaveClass("sticky", "top-0", "z-50");
|
||||
});
|
||||
});
|
||||
|
||||
describe("CSS Classes and Styling", () => {
|
||||
test("has correct CSS classes for styling", () => {
|
||||
const { container } = render(<Header />);
|
||||
|
||||
const header = container.querySelector(
|
||||
'[role="banner"][aria-label="Main navigation header"]',
|
||||
);
|
||||
expect(header).toHaveClass("bg-[var(--color-surface-default-primary)]");
|
||||
expect(header).toHaveClass("w-full");
|
||||
expect(header).toHaveClass("border-b");
|
||||
expect(header).toHaveClass(
|
||||
"border-[var(--border-color-default-tertiary)]",
|
||||
);
|
||||
|
||||
const nav = container.querySelector(
|
||||
'[role="navigation"][aria-label="Main navigation"]',
|
||||
);
|
||||
expect(nav).toHaveClass("flex");
|
||||
expect(nav).toHaveClass("items-center");
|
||||
expect(nav).toHaveClass("justify-between");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,141 +0,0 @@
|
||||
import { render, screen, cleanup } from "@testing-library/react";
|
||||
import { describe, test, expect, afterEach } from "vitest";
|
||||
import HeroBanner from "../../app/components/HeroBanner";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe("HeroBanner Component", () => {
|
||||
test("renders with all props", () => {
|
||||
render(
|
||||
<HeroBanner
|
||||
title="Welcome to CommunityRule"
|
||||
subtitle="Build better communities"
|
||||
description="Create and manage community rules with ease"
|
||||
ctaText="Get Started"
|
||||
ctaHref="/signup"
|
||||
/>,
|
||||
);
|
||||
|
||||
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();
|
||||
// Button component renders multiple versions for different screen sizes
|
||||
// Use getAllByRole to handle multiple buttons with same text
|
||||
const buttons = screen.getAllByRole("button", { name: "Get Started" });
|
||||
expect(buttons.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("renders with minimal props", () => {
|
||||
render(<HeroBanner title="Minimal" />);
|
||||
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "Minimal" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("img", { name: "Hero illustration" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders hero image", () => {
|
||||
render(<HeroBanner title="Test" />);
|
||||
|
||||
const heroImage = screen.getByRole("img", { name: "Hero illustration" });
|
||||
expect(heroImage).toBeInTheDocument();
|
||||
expect(heroImage).toHaveAttribute("src", "/assets/HeroImage.png");
|
||||
});
|
||||
|
||||
test("applies correct CSS classes", () => {
|
||||
render(<HeroBanner title="Test" />);
|
||||
|
||||
const section = document.querySelector("section");
|
||||
expect(section).toHaveClass("bg-transparent");
|
||||
|
||||
// Find the div with md:flex-1 class
|
||||
const contentLockup = document.querySelector('[class*="md:flex-1"]');
|
||||
expect(contentLockup).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders ContentLockup with correct props", () => {
|
||||
render(
|
||||
<HeroBanner
|
||||
title="Test Title"
|
||||
subtitle="Test Subtitle"
|
||||
description="Test Description"
|
||||
ctaText="Test CTA"
|
||||
ctaHref="/test"
|
||||
/>,
|
||||
);
|
||||
|
||||
// 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();
|
||||
// Button component renders multiple versions for different screen sizes
|
||||
const buttons = screen.getAllByRole("button", { name: "Test CTA" });
|
||||
expect(buttons.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("renders HeroDecor component", () => {
|
||||
render(<HeroBanner title="Test" />);
|
||||
|
||||
// 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(<HeroBanner title="Test" />);
|
||||
|
||||
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(<HeroBanner title="" />);
|
||||
|
||||
// Should still render the structure even with empty title
|
||||
const section = document.querySelector("section");
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("applies responsive design classes", () => {
|
||||
render(<HeroBanner title="Test" />);
|
||||
|
||||
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(<HeroBanner title="Test" />);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,271 +0,0 @@
|
||||
import React from "react";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { expect, test, describe, vi } from "vitest";
|
||||
import Input from "../../app/components/Input";
|
||||
|
||||
describe("Input Component", () => {
|
||||
test("renders with default props", () => {
|
||||
render(<Input />);
|
||||
const input = screen.getByRole("textbox");
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toHaveAttribute("type", "text");
|
||||
});
|
||||
|
||||
test("renders with label", () => {
|
||||
render(<Input label="Test input" />);
|
||||
expect(screen.getByText("Test input")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Test input")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with placeholder", () => {
|
||||
render(<Input placeholder="Enter text..." />);
|
||||
const input = screen.getByPlaceholderText("Enter text...");
|
||||
expect(input).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with value", () => {
|
||||
render(<Input value="test value" />);
|
||||
const input = screen.getByDisplayValue("test value");
|
||||
expect(input).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls onChange when text is entered", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<Input onChange={handleChange} />);
|
||||
|
||||
const input = screen.getByRole("textbox");
|
||||
fireEvent.change(input, { target: { value: "new text" } });
|
||||
|
||||
expect(handleChange).toHaveBeenCalledWith(expect.any(Object));
|
||||
});
|
||||
|
||||
test("calls onFocus when focused", () => {
|
||||
const handleFocus = vi.fn();
|
||||
render(<Input onFocus={handleFocus} />);
|
||||
|
||||
const input = screen.getByRole("textbox");
|
||||
fireEvent.focus(input);
|
||||
|
||||
expect(handleFocus).toHaveBeenCalledWith(expect.any(Object));
|
||||
});
|
||||
|
||||
test("calls onBlur when blurred", () => {
|
||||
const handleBlur = vi.fn();
|
||||
render(<Input onBlur={handleBlur} />);
|
||||
|
||||
const input = screen.getByRole("textbox");
|
||||
fireEvent.blur(input);
|
||||
|
||||
expect(handleBlur).toHaveBeenCalledWith(expect.any(Object));
|
||||
});
|
||||
|
||||
test("does not call onChange when disabled", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<Input disabled={true} onChange={handleChange} />);
|
||||
|
||||
const input = screen.getByRole("textbox");
|
||||
fireEvent.change(input, { target: { value: "new text" } });
|
||||
|
||||
expect(handleChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("does not call onFocus when disabled", () => {
|
||||
const handleFocus = vi.fn();
|
||||
render(<Input disabled={true} onFocus={handleFocus} />);
|
||||
|
||||
const input = screen.getByRole("textbox");
|
||||
fireEvent.focus(input);
|
||||
|
||||
expect(handleFocus).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("does not call onBlur when disabled", () => {
|
||||
const handleBlur = vi.fn();
|
||||
render(<Input disabled={true} onBlur={handleBlur} />);
|
||||
|
||||
const input = screen.getByRole("textbox");
|
||||
fireEvent.blur(input);
|
||||
|
||||
expect(handleBlur).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("applies disabled attributes when disabled", () => {
|
||||
render(<Input disabled={true} />);
|
||||
const input = screen.getByRole("textbox");
|
||||
expect(input).toBeDisabled();
|
||||
});
|
||||
|
||||
test("applies correct size classes", () => {
|
||||
const { rerender } = render(<Input size="small" />);
|
||||
let input = screen.getByRole("textbox");
|
||||
expect(input).toHaveClass("h-[32px]");
|
||||
|
||||
rerender(<Input size="medium" />);
|
||||
input = screen.getByRole("textbox");
|
||||
expect(input).toHaveClass("h-[36px]");
|
||||
|
||||
rerender(<Input size="large" />);
|
||||
input = screen.getByRole("textbox");
|
||||
expect(input).toHaveClass("h-[40px]");
|
||||
});
|
||||
|
||||
test("applies correct label variant classes", () => {
|
||||
const { rerender } = render(<Input label="Test" labelVariant="default" />);
|
||||
let container = screen.getByRole("textbox").closest("div").parentElement;
|
||||
expect(container).toHaveClass("flex-col");
|
||||
|
||||
rerender(<Input label="Test" labelVariant="horizontal" />);
|
||||
container = screen.getByRole("textbox").closest("div").parentElement;
|
||||
expect(container).toHaveClass("flex", "items-center");
|
||||
});
|
||||
|
||||
test("applies error state classes", () => {
|
||||
render(<Input error={true} />);
|
||||
const input = screen.getByRole("textbox");
|
||||
expect(input).toHaveClass(
|
||||
"border-[var(--color-border-default-utility-negative)]",
|
||||
);
|
||||
});
|
||||
|
||||
test("applies disabled state classes", () => {
|
||||
render(<Input disabled={true} />);
|
||||
const input = screen.getByRole("textbox");
|
||||
expect(input).toHaveClass("cursor-not-allowed");
|
||||
expect(input).toHaveClass("bg-[var(--color-content-default-secondary)]");
|
||||
});
|
||||
|
||||
test("applies focus state classes", () => {
|
||||
render(<Input state="focus" />);
|
||||
const input = screen.getByRole("textbox");
|
||||
expect(input).toHaveClass(
|
||||
"border-[var(--color-border-default-utility-info)]",
|
||||
);
|
||||
expect(input).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
|
||||
});
|
||||
|
||||
test("applies hover state classes", () => {
|
||||
render(<Input state="hover" />);
|
||||
const input = screen.getByRole("textbox");
|
||||
expect(input).toHaveClass("border-[var(--color-border-default-tertiary)]");
|
||||
expect(input).toHaveClass(
|
||||
"shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
|
||||
);
|
||||
});
|
||||
|
||||
test("applies active state classes", () => {
|
||||
render(<Input state="active" />);
|
||||
const input = screen.getByRole("textbox");
|
||||
expect(input).toHaveClass("border-[var(--color-border-default-tertiary)]");
|
||||
});
|
||||
|
||||
test("applies default state classes", () => {
|
||||
render(<Input state="default" />);
|
||||
const input = screen.getByRole("textbox");
|
||||
expect(input).toHaveClass("border-[var(--color-border-default-tertiary)]");
|
||||
expect(input).toHaveClass(
|
||||
"hover:shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
|
||||
);
|
||||
});
|
||||
|
||||
test("applies custom className", () => {
|
||||
render(<Input className="custom-class" />);
|
||||
const input = screen.getByRole("textbox");
|
||||
expect(input).toHaveClass("custom-class");
|
||||
});
|
||||
|
||||
test("passes through additional props", () => {
|
||||
render(<Input id="test-input" name="test" type="email" />);
|
||||
const input = screen.getByRole("textbox");
|
||||
expect(input).toHaveAttribute("id", "test-input");
|
||||
expect(input).toHaveAttribute("name", "test");
|
||||
expect(input).toHaveAttribute("type", "email");
|
||||
});
|
||||
|
||||
test("generates unique ID when not provided", () => {
|
||||
render(<Input label="Test" />);
|
||||
const input = screen.getByRole("textbox");
|
||||
const label = screen.getByText("Test");
|
||||
expect(input).toHaveAttribute("id");
|
||||
expect(label).toHaveAttribute("for", input.id);
|
||||
});
|
||||
|
||||
test("uses provided ID when given", () => {
|
||||
render(<Input id="custom-id" label="Test" />);
|
||||
const input = screen.getByRole("textbox");
|
||||
const label = screen.getByText("Test");
|
||||
expect(input).toHaveAttribute("id", "custom-id");
|
||||
expect(label).toHaveAttribute("for", "custom-id");
|
||||
});
|
||||
|
||||
test("applies correct border radius style", () => {
|
||||
const { rerender } = render(<Input size="small" />);
|
||||
let input = screen.getByRole("textbox");
|
||||
expect(input).toHaveStyle("border-radius: var(--measures-radius-small)");
|
||||
|
||||
rerender(<Input size="medium" />);
|
||||
input = screen.getByRole("textbox");
|
||||
expect(input).toHaveStyle("border-radius: var(--measures-radius-medium)");
|
||||
|
||||
rerender(<Input size="large" />);
|
||||
input = screen.getByRole("textbox");
|
||||
expect(input).toHaveStyle("border-radius: var(--measures-radius-large)");
|
||||
});
|
||||
|
||||
test("applies opacity wrapper when disabled", () => {
|
||||
render(<Input disabled={true} />);
|
||||
const wrapper = screen.getByRole("textbox").closest("div");
|
||||
expect(wrapper).toHaveClass("opacity-40");
|
||||
});
|
||||
|
||||
test("does not apply opacity wrapper when not disabled", () => {
|
||||
render(<Input disabled={false} />);
|
||||
const wrapper = screen.getByRole("textbox").closest("div");
|
||||
expect(wrapper).not.toHaveClass("opacity-40");
|
||||
});
|
||||
|
||||
test("applies correct label styling", () => {
|
||||
render(<Input label="Test label" size="small" />);
|
||||
const label = screen.getByText("Test label");
|
||||
expect(label).toHaveClass("text-[12px]");
|
||||
expect(label).toHaveClass("leading-[14px]");
|
||||
expect(label).toHaveClass("font-medium");
|
||||
expect(label).toHaveClass("text-[var(--color-content-default-secondary)]");
|
||||
});
|
||||
|
||||
test("applies correct input text styling for different sizes", () => {
|
||||
const { rerender } = render(<Input size="small" />);
|
||||
let input = screen.getByRole("textbox");
|
||||
expect(input).toHaveClass("text-[10px]");
|
||||
|
||||
rerender(<Input size="medium" />);
|
||||
input = screen.getByRole("textbox");
|
||||
expect(input).toHaveClass("text-[14px]");
|
||||
expect(input).toHaveClass("leading-[20px]");
|
||||
|
||||
rerender(<Input size="large" />);
|
||||
input = screen.getByRole("textbox");
|
||||
expect(input).toHaveClass("text-[16px]");
|
||||
expect(input).toHaveClass("leading-[24px]");
|
||||
});
|
||||
|
||||
test("handles keyboard navigation", () => {
|
||||
const handleFocus = vi.fn();
|
||||
render(<Input onFocus={handleFocus} />);
|
||||
|
||||
const input = screen.getByRole("textbox");
|
||||
fireEvent.keyDown(input, { key: "Tab" });
|
||||
fireEvent.focus(input);
|
||||
|
||||
expect(handleFocus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("forwards ref correctly", () => {
|
||||
const ref = React.createRef();
|
||||
render(<Input ref={ref} />);
|
||||
expect(ref.current).toBeInstanceOf(HTMLInputElement);
|
||||
});
|
||||
|
||||
test("is memoized", () => {
|
||||
expect(Input.$$typeof).toBe(Symbol.for("react.memo"));
|
||||
});
|
||||
});
|
||||
@@ -1,128 +0,0 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import Logo from "../../app/components/Logo";
|
||||
|
||||
describe("Logo Component", () => {
|
||||
it("renders the logo with default props", () => {
|
||||
render(<Logo />);
|
||||
|
||||
const logo = screen.getByRole("link", { name: /communityrule logo/i });
|
||||
expect(logo).toBeInTheDocument();
|
||||
expect(screen.getByText("CommunityRule")).toBeInTheDocument();
|
||||
expect(screen.getByAltText("CommunityRule Logo Icon")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders with custom size variant", () => {
|
||||
const { rerender } = render(<Logo size="header" />);
|
||||
let logoDiv = screen.getByRole("link").querySelector("div");
|
||||
expect(logoDiv).toHaveClass("h-[20.85px]");
|
||||
|
||||
rerender(<Logo size="headerLg" />);
|
||||
logoDiv = screen.getByRole("link").querySelector("div");
|
||||
expect(logoDiv).toHaveClass("h-[28px]");
|
||||
|
||||
rerender(<Logo size="footer" />);
|
||||
logoDiv = screen.getByRole("link").querySelector("div");
|
||||
expect(logoDiv).toHaveClass("h-[calc(40px*1.37)]");
|
||||
});
|
||||
|
||||
it("renders without text when showText is false", () => {
|
||||
render(<Logo showText={false} />);
|
||||
|
||||
expect(screen.queryByText("CommunityRule")).not.toBeInTheDocument();
|
||||
expect(screen.getByAltText("CommunityRule Logo Icon")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies proper hover effects", () => {
|
||||
render(<Logo />);
|
||||
|
||||
const logoDiv = screen.getByRole("link").querySelector("div");
|
||||
expect(logoDiv).toHaveClass("hover:scale-[1.02]", "transition-all");
|
||||
});
|
||||
|
||||
it("applies proper accessibility attributes", () => {
|
||||
render(<Logo />);
|
||||
|
||||
const logo = screen.getByRole("link");
|
||||
expect(logo).toHaveAttribute("aria-label", "CommunityRule Logo");
|
||||
expect(logo).toHaveAttribute("href", "/");
|
||||
});
|
||||
|
||||
it("applies proper text styling for different sizes", () => {
|
||||
const { rerender } = render(<Logo size="homeHeaderMd" />);
|
||||
let textElement = screen.getByText("CommunityRule");
|
||||
expect(textElement).toHaveClass(
|
||||
"text-[var(--color-content-inverse-primary)]",
|
||||
);
|
||||
|
||||
rerender(<Logo size="header" />);
|
||||
textElement = screen.getByText("CommunityRule");
|
||||
expect(textElement).toHaveClass(
|
||||
"text-[var(--color-content-default-primary)]",
|
||||
);
|
||||
});
|
||||
|
||||
it("applies proper icon sizing for different variants", () => {
|
||||
const { rerender } = render(<Logo size="homeHeaderSm" />);
|
||||
let icon = screen.getByAltText("CommunityRule Logo Icon");
|
||||
expect(icon).toHaveClass("w-[14.39px]", "h-[14.39px]");
|
||||
|
||||
rerender(<Logo size="headerXl" />);
|
||||
icon = screen.getByAltText("CommunityRule Logo Icon");
|
||||
expect(icon).toHaveClass("w-[33.81px]", "h-[33.81px]");
|
||||
});
|
||||
|
||||
it("applies brightness filter for home header variants", () => {
|
||||
render(<Logo size="homeHeaderMd" />);
|
||||
|
||||
const icon = screen.getByAltText("CommunityRule Logo Icon");
|
||||
expect(icon).toHaveClass("filter", "brightness-0");
|
||||
});
|
||||
|
||||
it("maintains proper spacing when text is hidden", () => {
|
||||
render(<Logo showText={false} />);
|
||||
|
||||
const logo = screen.getByRole("link");
|
||||
// Should not have gap class when text is hidden
|
||||
expect(logo.className).not.toContain("gap-[8.28px]");
|
||||
});
|
||||
|
||||
it("applies proper font classes to text", () => {
|
||||
render(<Logo />);
|
||||
|
||||
const textElement = screen.getByText("CommunityRule");
|
||||
expect(textElement).toHaveClass("font-bricolage-grotesque", "font-normal");
|
||||
});
|
||||
|
||||
it("applies proper icon attributes", () => {
|
||||
render(<Logo />);
|
||||
|
||||
const icon = screen.getByAltText("CommunityRule Logo Icon");
|
||||
expect(icon).toHaveAttribute("src", "/assets/Logo.svg");
|
||||
expect(icon).toHaveAttribute("aria-hidden", "true");
|
||||
});
|
||||
|
||||
it("handles all size variants correctly", () => {
|
||||
const sizes = [
|
||||
"default",
|
||||
"homeHeaderXsmall",
|
||||
"homeHeaderSm",
|
||||
"homeHeaderMd",
|
||||
"homeHeaderLg",
|
||||
"homeHeaderXl",
|
||||
"header",
|
||||
"headerMd",
|
||||
"headerLg",
|
||||
"headerXl",
|
||||
"footer",
|
||||
"footerLg",
|
||||
];
|
||||
|
||||
sizes.forEach((size) => {
|
||||
const { unmount } = render(<Logo size={size} />);
|
||||
const logo = screen.getByRole("link");
|
||||
expect(logo).toBeInTheDocument();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,283 +0,0 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import Page from "../../app/page";
|
||||
|
||||
describe("Page", () => {
|
||||
test("renders all main sections", async () => {
|
||||
render(<Page />);
|
||||
|
||||
// Check that all main sections are rendered (using getAllByText since there are multiple instances)
|
||||
expect(screen.getAllByText("Collaborate").length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText("with clarity").length).toBeGreaterThan(0);
|
||||
expect(
|
||||
screen.getAllByText(
|
||||
"Help your community make important decisions in a way that reflects its unique values.",
|
||||
).length,
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
// Wait for dynamically imported components to load
|
||||
// Check numbered cards section (using getAllByText since there are multiple instances)
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getAllByText("How CommunityRule works").length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
expect(
|
||||
screen.getAllByText(
|
||||
"Here's a quick overview of the process, from start to finish.",
|
||||
).length,
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
// Check feature grid section (using getAllByText since there are multiple instances)
|
||||
expect(
|
||||
screen.getAllByText("We've got your back, every step of the way").length,
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
screen.getAllByText(
|
||||
"Use our toolkit to improve, document, and evolve your organization.",
|
||||
).length,
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
// Check ask organizer section (using getAllByText since there are multiple instances)
|
||||
expect(screen.getAllByText("Still have questions?").length).toBeGreaterThan(
|
||||
0,
|
||||
);
|
||||
expect(
|
||||
screen.getAllByText("Get answers from an experienced organizer").length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("renders hero banner with correct data", () => {
|
||||
render(<Page />);
|
||||
|
||||
// Check hero banner content (using getAllByText since there are multiple instances)
|
||||
expect(screen.getAllByText("Collaborate").length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText("with clarity").length).toBeGreaterThan(0);
|
||||
expect(
|
||||
screen.getAllByText(
|
||||
"Help your community make important decisions in a way that reflects its unique values.",
|
||||
).length,
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
screen.getAllByText("Learn how CommunityRule works").length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("renders numbered cards with correct data", async () => {
|
||||
render(<Page />);
|
||||
|
||||
// Wait for dynamically imported NumberedCards component to load
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getAllByText("How CommunityRule works").length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
expect(
|
||||
screen.getAllByText(
|
||||
"Here's a quick overview of the process, from start to finish.",
|
||||
).length,
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
// Check individual card content (using getAllByText since there are multiple instances)
|
||||
expect(
|
||||
screen.getAllByText("Document how your community makes decisions").length,
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
screen.getAllByText(
|
||||
"Build an operating manual for a successful community",
|
||||
).length,
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
screen.getAllByText(
|
||||
"Get a link to your manual for your group to review and evolve",
|
||||
).length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("renders feature grid with correct data", async () => {
|
||||
render(<Page />);
|
||||
|
||||
// Wait for dynamically imported FeatureGrid component to load
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getAllByText("We've got your back, every step of the way")
|
||||
.length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
expect(
|
||||
screen.getAllByText(
|
||||
"Use our toolkit to improve, document, and evolve your organization.",
|
||||
).length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("renders ask organizer section with correct data", () => {
|
||||
render(<Page />);
|
||||
|
||||
// Check ask organizer content (using getAllByText since there are multiple instances)
|
||||
expect(screen.getAllByText("Still have questions?").length).toBeGreaterThan(
|
||||
0,
|
||||
);
|
||||
expect(
|
||||
screen.getAllByText("Get answers from an experienced organizer").length,
|
||||
).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText("Ask an organizer").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("renders all component sections", async () => {
|
||||
render(<Page />);
|
||||
|
||||
// Check that all major components are present by looking for their content
|
||||
// HeroBanner
|
||||
expect(screen.getAllByText("Collaborate").length).toBeGreaterThan(0);
|
||||
|
||||
// Wait for dynamically imported components to load
|
||||
// LogoWall - should be present (even if just the component structure)
|
||||
// NumberedCards
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getAllByText("How CommunityRule works").length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// RuleStack - should be present
|
||||
// FeatureGrid
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getAllByText("We've got your back, every step of the way")
|
||||
.length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// QuoteBlock - should be present
|
||||
// AskOrganizer
|
||||
expect(screen.getAllByText("Still have questions?").length).toBeGreaterThan(
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
test("has correct page structure", () => {
|
||||
render(<Page />);
|
||||
|
||||
const mainContainer = screen.getAllByText("Collaborate")[0].closest("div");
|
||||
expect(mainContainer).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders call-to-action elements", () => {
|
||||
render(<Page />);
|
||||
|
||||
// Check CTA button in hero banner
|
||||
expect(
|
||||
screen.getAllByText("Learn how CommunityRule works").length,
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
// Check CTA button in ask organizer section
|
||||
expect(screen.getAllByText("Ask an organizer").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("renders descriptive text content", async () => {
|
||||
render(<Page />);
|
||||
|
||||
// Check main description (using getAllByText since there are multiple instances)
|
||||
expect(
|
||||
screen.getAllByText(
|
||||
"Help your community make important decisions in a way that reflects its unique values.",
|
||||
).length,
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
// Wait for dynamically imported NumberedCards component
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getAllByText(
|
||||
"Here's a quick overview of the process, from start to finish.",
|
||||
).length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// Wait for dynamically imported FeatureGrid component
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getAllByText(
|
||||
"Use our toolkit to improve, document, and evolve your organization.",
|
||||
).length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// Check ask organizer description (using getAllByText since there are multiple instances)
|
||||
expect(
|
||||
screen.getAllByText("Get answers from an experienced organizer").length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("renders section titles correctly", async () => {
|
||||
render(<Page />);
|
||||
|
||||
// Check all section titles (using getAllByText since there are multiple instances)
|
||||
expect(screen.getAllByText("Collaborate").length).toBeGreaterThan(0);
|
||||
|
||||
// Wait for dynamically imported components
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getAllByText("How CommunityRule works").length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getAllByText("We've got your back, every step of the way")
|
||||
.length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
expect(screen.getAllByText("Still have questions?").length).toBeGreaterThan(
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
test("renders numbered card items with correct content", async () => {
|
||||
render(<Page />);
|
||||
|
||||
// Wait for dynamically imported NumberedCards component
|
||||
await waitFor(() => {
|
||||
// Check all three numbered card items (using getAllByText since there are multiple instances)
|
||||
expect(
|
||||
screen.getAllByText("Document how your community makes decisions")
|
||||
.length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
expect(
|
||||
screen.getAllByText(
|
||||
"Build an operating manual for a successful community",
|
||||
).length,
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
screen.getAllByText(
|
||||
"Get a link to your manual for your group to review and evolve",
|
||||
).length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("renders subtitle content correctly", async () => {
|
||||
render(<Page />);
|
||||
|
||||
// Check subtitles (using getAllByText since there are multiple instances)
|
||||
expect(screen.getAllByText("with clarity").length).toBeGreaterThan(0);
|
||||
|
||||
// Wait for dynamically imported components
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getAllByText(
|
||||
"Here's a quick overview of the process, from start to finish.",
|
||||
).length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getAllByText(
|
||||
"Use our toolkit to improve, document, and evolve your organization.",
|
||||
).length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
expect(
|
||||
screen.getAllByText("Get answers from an experienced organizer").length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -1,248 +0,0 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import RadioButton from "../../app/components/RadioButton";
|
||||
|
||||
describe("RadioButton", () => {
|
||||
it("renders with default props", () => {
|
||||
render(<RadioButton />);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
expect(radioButton).toBeInTheDocument();
|
||||
expect(radioButton).toHaveAttribute("aria-checked", "false");
|
||||
});
|
||||
|
||||
it("renders with label", () => {
|
||||
render(<RadioButton label="Test Radio" />);
|
||||
|
||||
expect(screen.getByText("Test Radio")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows checked state", () => {
|
||||
render(<RadioButton checked={true} label="Checked Radio" />);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
expect(radioButton).toHaveAttribute("aria-checked", "true");
|
||||
});
|
||||
|
||||
it("calls onChange when clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleChange = vi.fn();
|
||||
|
||||
render(
|
||||
<RadioButton
|
||||
checked={false}
|
||||
onChange={handleChange}
|
||||
label="Test Radio"
|
||||
/>,
|
||||
);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
await user.click(radioButton);
|
||||
|
||||
expect(handleChange).toHaveBeenCalledWith({
|
||||
checked: true,
|
||||
value: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("calls onChange with value when clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleChange = vi.fn();
|
||||
|
||||
render(
|
||||
<RadioButton
|
||||
checked={false}
|
||||
value="test-value"
|
||||
onChange={handleChange}
|
||||
label="Test Radio"
|
||||
/>,
|
||||
);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
await user.click(radioButton);
|
||||
|
||||
expect(handleChange).toHaveBeenCalledWith({
|
||||
checked: true,
|
||||
value: "test-value",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not call onChange when clicking already checked radio button", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleChange = vi.fn();
|
||||
|
||||
render(
|
||||
<RadioButton checked={true} onChange={handleChange} label="Test Radio" />,
|
||||
);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
await user.click(radioButton);
|
||||
|
||||
// Radio buttons should not be unchecked by clicking them again
|
||||
expect(handleChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("handles keyboard activation", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleChange = vi.fn();
|
||||
|
||||
render(
|
||||
<RadioButton
|
||||
checked={false}
|
||||
onChange={handleChange}
|
||||
label="Test Radio"
|
||||
/>,
|
||||
);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
radioButton.focus();
|
||||
await user.keyboard(" ");
|
||||
|
||||
expect(handleChange).toHaveBeenCalledWith({
|
||||
checked: true,
|
||||
value: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("handles Enter key activation", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleChange = vi.fn();
|
||||
|
||||
render(
|
||||
<RadioButton
|
||||
checked={false}
|
||||
onChange={handleChange}
|
||||
label="Test Radio"
|
||||
/>,
|
||||
);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
await user.click(radioButton); // Focus the element first
|
||||
await user.keyboard("{Enter}");
|
||||
|
||||
expect(handleChange).toHaveBeenCalledWith({
|
||||
checked: true,
|
||||
value: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("applies standard mode classes", () => {
|
||||
render(<RadioButton mode="standard" label="Standard Radio" />);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
expect(radioButton).toHaveClass(
|
||||
"outline-[var(--color-border-default-tertiary)]",
|
||||
);
|
||||
});
|
||||
|
||||
it("applies inverse mode classes", () => {
|
||||
render(<RadioButton mode="inverse" label="Inverse Radio" />);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
expect(radioButton).toHaveClass(
|
||||
"outline-[var(--color-border-inverse-primary)]",
|
||||
);
|
||||
});
|
||||
|
||||
it("applies focus state classes", () => {
|
||||
render(<RadioButton state="focus" label="Focus Radio" />);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
expect(radioButton).toHaveClass("focus:outline");
|
||||
});
|
||||
|
||||
it("applies hover state classes", () => {
|
||||
render(<RadioButton state="hover" label="Hover Radio" />);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
expect(radioButton).toHaveClass("hover:outline");
|
||||
});
|
||||
|
||||
it("renders hidden input for form submission", () => {
|
||||
render(
|
||||
<RadioButton
|
||||
name="test-radio"
|
||||
value="test-value"
|
||||
checked={true}
|
||||
label="Test Radio"
|
||||
/>,
|
||||
);
|
||||
|
||||
const hiddenInput = screen.getByDisplayValue("test-value");
|
||||
expect(hiddenInput).toBeInTheDocument();
|
||||
expect(hiddenInput).toHaveAttribute("type", "radio");
|
||||
expect(hiddenInput).toHaveAttribute("name", "test-radio");
|
||||
expect(hiddenInput).toBeChecked();
|
||||
});
|
||||
|
||||
it("applies custom className", () => {
|
||||
render(<RadioButton className="custom-class" label="Custom Radio" />);
|
||||
|
||||
const label = screen.getByText("Custom Radio").closest("label");
|
||||
expect(label).toHaveClass("custom-class");
|
||||
});
|
||||
|
||||
it("generates unique ID when not provided", () => {
|
||||
render(<RadioButton label="Radio 1" />);
|
||||
render(<RadioButton label="Radio 2" />);
|
||||
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
expect(radioButtons[0]).toHaveAttribute("id");
|
||||
expect(radioButtons[1]).toHaveAttribute("id");
|
||||
expect(radioButtons[0].id).not.toBe(radioButtons[1].id);
|
||||
});
|
||||
|
||||
it("uses provided ID", () => {
|
||||
render(<RadioButton id="custom-id" label="Custom ID Radio" />);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
expect(radioButton).toHaveAttribute("id", "custom-id");
|
||||
});
|
||||
|
||||
it("associates label with radio button for accessibility", () => {
|
||||
render(<RadioButton label="Accessible Radio" />);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
const labelId = radioButton.getAttribute("aria-labelledby");
|
||||
expect(labelId).toBeTruthy();
|
||||
|
||||
const labelElement = document.getElementById(labelId);
|
||||
expect(labelElement).toHaveTextContent("Accessible Radio");
|
||||
});
|
||||
|
||||
it("uses aria-label when provided", () => {
|
||||
render(<RadioButton ariaLabel="Custom Aria Label" />);
|
||||
|
||||
const radioButton = screen.getByRole("radio");
|
||||
expect(radioButton).toHaveAttribute("aria-label", "Custom Aria Label");
|
||||
});
|
||||
|
||||
it("shows dot indicator when checked", () => {
|
||||
render(
|
||||
<RadioButton checked={true} mode="standard" label="Checked Radio" />,
|
||||
);
|
||||
|
||||
const dot = screen.getByRole("radio").querySelector("div");
|
||||
expect(dot).toHaveClass("w-[16px]", "h-[16px]", "rounded-full");
|
||||
});
|
||||
|
||||
it("hides dot indicator when unchecked", () => {
|
||||
render(
|
||||
<RadioButton checked={false} mode="standard" label="Unchecked Radio" />,
|
||||
);
|
||||
|
||||
const dot = screen.getByRole("radio").querySelector("div");
|
||||
// Check if the dot has transparent background or no background color set
|
||||
const computedStyle = window.getComputedStyle(dot);
|
||||
const backgroundColor = computedStyle.backgroundColor;
|
||||
|
||||
// The dot should either be transparent or have no background color
|
||||
expect(
|
||||
backgroundColor === "transparent" ||
|
||||
backgroundColor === "rgba(0, 0, 0, 0)" ||
|
||||
backgroundColor === "",
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,240 +0,0 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import RadioGroup from "../../app/components/RadioGroup";
|
||||
|
||||
describe("RadioGroup", () => {
|
||||
const defaultOptions = [
|
||||
{ value: "option1", label: "Option 1" },
|
||||
{ value: "option2", label: "Option 2" },
|
||||
{ value: "option3", label: "Option 3" },
|
||||
];
|
||||
|
||||
it("renders with default props", () => {
|
||||
render(<RadioGroup options={defaultOptions} />);
|
||||
|
||||
const radioGroup = screen.getByRole("radiogroup");
|
||||
expect(radioGroup).toBeInTheDocument();
|
||||
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
expect(radioButtons).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("renders all options", () => {
|
||||
render(<RadioGroup options={defaultOptions} />);
|
||||
|
||||
expect(screen.getByText("Option 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Option 2")).toBeInTheDocument();
|
||||
expect(screen.getByText("Option 3")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows selected option", () => {
|
||||
render(<RadioGroup options={defaultOptions} value="option2" />);
|
||||
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
|
||||
expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
|
||||
expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
|
||||
});
|
||||
|
||||
it("calls onChange when option is selected", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleChange = vi.fn();
|
||||
|
||||
render(
|
||||
<RadioGroup
|
||||
options={defaultOptions}
|
||||
value="option1"
|
||||
onChange={handleChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const option2 = screen.getByText("Option 2").closest("label");
|
||||
await user.click(option2);
|
||||
|
||||
expect(handleChange).toHaveBeenCalledWith({ value: "option2" });
|
||||
});
|
||||
|
||||
it("updates selection when different option is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleChange = vi.fn();
|
||||
|
||||
render(
|
||||
<RadioGroup
|
||||
options={defaultOptions}
|
||||
value="option1"
|
||||
onChange={handleChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Click option 3
|
||||
const option3 = screen.getByText("Option 3").closest("label");
|
||||
await user.click(option3);
|
||||
|
||||
expect(handleChange).toHaveBeenCalledWith({ value: "option3" });
|
||||
});
|
||||
|
||||
it("handles keyboard navigation", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleChange = vi.fn();
|
||||
|
||||
render(
|
||||
<RadioGroup
|
||||
options={defaultOptions}
|
||||
value="option1"
|
||||
onChange={handleChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
radioButtons[1].focus();
|
||||
await user.keyboard(" ");
|
||||
|
||||
expect(handleChange).toHaveBeenCalledWith({ value: "option2" });
|
||||
});
|
||||
|
||||
it("handles Enter key activation", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleChange = vi.fn();
|
||||
|
||||
render(
|
||||
<RadioGroup
|
||||
options={defaultOptions}
|
||||
value="option1"
|
||||
onChange={handleChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
await user.click(radioButtons[2]);
|
||||
await user.keyboard("{Enter}");
|
||||
|
||||
expect(handleChange).toHaveBeenCalledWith({ value: "option3" });
|
||||
});
|
||||
|
||||
it("applies standard mode to all radio buttons", () => {
|
||||
render(<RadioGroup options={defaultOptions} mode="standard" />);
|
||||
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
radioButtons.forEach((button) => {
|
||||
expect(button).toHaveClass(
|
||||
"outline-[var(--color-border-default-tertiary)]",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("applies inverse mode to all radio buttons", () => {
|
||||
render(<RadioGroup options={defaultOptions} mode="inverse" />);
|
||||
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
radioButtons.forEach((button) => {
|
||||
expect(button).toHaveClass(
|
||||
"outline-[var(--color-border-inverse-primary)]",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("applies state to all radio buttons", () => {
|
||||
render(<RadioGroup options={defaultOptions} state="focus" />);
|
||||
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
radioButtons.forEach((button) => {
|
||||
expect(button).toHaveClass("focus:outline");
|
||||
});
|
||||
});
|
||||
|
||||
it("generates unique group name when not provided", () => {
|
||||
render(<RadioGroup options={defaultOptions} />);
|
||||
render(<RadioGroup options={defaultOptions} />);
|
||||
|
||||
const hiddenInputs = screen.getAllByRole("radio", { hidden: true });
|
||||
const names = hiddenInputs.map((input) => input.getAttribute("name"));
|
||||
|
||||
// Should have unique names
|
||||
const uniqueNames = new Set(names);
|
||||
expect(uniqueNames.size).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
it("uses provided name for all radio buttons", () => {
|
||||
render(<RadioGroup options={defaultOptions} name="test-group" />);
|
||||
|
||||
const hiddenInputs = screen.getAllByDisplayValue("option1");
|
||||
hiddenInputs.forEach((input) => {
|
||||
expect(input).toHaveAttribute("name", "test-group");
|
||||
});
|
||||
});
|
||||
|
||||
it("applies custom className to container", () => {
|
||||
render(<RadioGroup options={defaultOptions} className="custom-group" />);
|
||||
|
||||
const radioGroup = screen.getByRole("radiogroup");
|
||||
expect(radioGroup).toHaveClass("custom-group");
|
||||
});
|
||||
|
||||
it("passes aria-label to radiogroup", () => {
|
||||
render(
|
||||
<RadioGroup options={defaultOptions} aria-label="Test Radio Group" />,
|
||||
);
|
||||
|
||||
const radioGroup = screen.getByRole("radiogroup");
|
||||
expect(radioGroup).toHaveAttribute("aria-label", "Test Radio Group");
|
||||
});
|
||||
|
||||
it("handles empty options array", () => {
|
||||
render(<RadioGroup options={[]} />);
|
||||
|
||||
const radioGroup = screen.getByRole("radiogroup");
|
||||
expect(radioGroup).toBeInTheDocument();
|
||||
|
||||
const radioButtons = screen.queryAllByRole("radio");
|
||||
expect(radioButtons).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("handles options with ariaLabel", () => {
|
||||
const optionsWithAria = [
|
||||
{ value: "option1", label: "Option 1", ariaLabel: "First Option" },
|
||||
{ value: "option2", label: "Option 2", ariaLabel: "Second Option" },
|
||||
];
|
||||
|
||||
render(<RadioGroup options={optionsWithAria} />);
|
||||
|
||||
const radioButtons = screen.getAllByRole("radio");
|
||||
expect(radioButtons[0]).toHaveAttribute("aria-label", "First Option");
|
||||
expect(radioButtons[1]).toHaveAttribute("aria-label", "Second Option");
|
||||
});
|
||||
|
||||
it("maintains selection state correctly", () => {
|
||||
const { rerender } = render(
|
||||
<RadioGroup options={defaultOptions} value="option1" />,
|
||||
);
|
||||
|
||||
let radioButtons = screen.getAllByRole("radio");
|
||||
expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
|
||||
|
||||
rerender(<RadioGroup options={defaultOptions} value="option3" />);
|
||||
|
||||
radioButtons = screen.getAllByRole("radio");
|
||||
expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
|
||||
expect(radioButtons[2]).toHaveAttribute("aria-checked", "true");
|
||||
});
|
||||
|
||||
it("does not call onChange when clicking already selected option", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleChange = vi.fn();
|
||||
|
||||
render(
|
||||
<RadioGroup
|
||||
options={defaultOptions}
|
||||
value="option2"
|
||||
onChange={handleChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const option2 = screen.getByText("Option 2").closest("label");
|
||||
await user.click(option2);
|
||||
|
||||
// Should not call onChange since it's already selected
|
||||
expect(handleChange).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,395 +0,0 @@
|
||||
import { describe, expect, vi, beforeEach, it } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import RelatedArticles from "../../app/components/RelatedArticles";
|
||||
|
||||
// Mock Next.js components
|
||||
vi.mock("next/link", () => {
|
||||
return {
|
||||
default: ({ children, href, ...props }) => (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock ContentThumbnailTemplate
|
||||
vi.mock("../../app/components/ContentThumbnailTemplate", () => {
|
||||
return {
|
||||
default: ({ post }) => (
|
||||
<div data-testid={`thumbnail-${post.slug}`}>
|
||||
<a href={`/blog/${post.slug}`}>
|
||||
<h3>{post.frontmatter.title}</h3>
|
||||
<p>{post.frontmatter.description}</p>
|
||||
</a>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock blog post data
|
||||
const mockRelatedPosts = [
|
||||
{
|
||||
slug: "related-article-1",
|
||||
frontmatter: {
|
||||
title: "Related Article 1",
|
||||
description: "This is the first related article",
|
||||
author: "Test Author",
|
||||
date: "2025-04-10",
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: "related-article-2",
|
||||
frontmatter: {
|
||||
title: "Related Article 2",
|
||||
description: "This is the second related article",
|
||||
author: "Test Author",
|
||||
date: "2025-04-12",
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: "related-article-3",
|
||||
frontmatter: {
|
||||
title: "Related Article 3",
|
||||
description: "This is the third related article",
|
||||
author: "Test Author",
|
||||
date: "2025-04-14",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe("RelatedArticles", () => {
|
||||
beforeEach(() => {
|
||||
// Mock window.innerWidth for responsive tests
|
||||
Object.defineProperty(window, "innerWidth", {
|
||||
writable: true,
|
||||
configurable: true,
|
||||
value: 1024, // Desktop width
|
||||
});
|
||||
});
|
||||
|
||||
it("renders the section with correct structure", () => {
|
||||
render(
|
||||
<RelatedArticles
|
||||
relatedPosts={mockRelatedPosts}
|
||||
currentPostSlug="current-article"
|
||||
/>,
|
||||
);
|
||||
|
||||
const section = document.querySelector("section");
|
||||
expect(section).toBeInTheDocument();
|
||||
expect(section).toHaveClass(
|
||||
"py-[var(--spacing-scale-032)]",
|
||||
"lg:py-[var(--spacing-scale-064)]",
|
||||
);
|
||||
});
|
||||
|
||||
it("displays the section heading", () => {
|
||||
render(
|
||||
<RelatedArticles
|
||||
relatedPosts={mockRelatedPosts}
|
||||
currentPostSlug="current-article"
|
||||
/>,
|
||||
);
|
||||
|
||||
const heading = screen.getByRole("heading", { level: 2 });
|
||||
expect(heading).toBeInTheDocument();
|
||||
expect(heading).toHaveTextContent("Related Articles");
|
||||
expect(heading).toHaveClass(
|
||||
"text-[32px]",
|
||||
"lg:text-[44px]",
|
||||
"leading-[110%]",
|
||||
"font-medium",
|
||||
"text-[var(--color-content-inverse-primary)]",
|
||||
"text-center",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders all related articles", () => {
|
||||
render(
|
||||
<RelatedArticles
|
||||
relatedPosts={mockRelatedPosts}
|
||||
currentPostSlug="current-article"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByTestId("thumbnail-related-article-1"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId("thumbnail-related-article-2"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId("thumbnail-related-article-3"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("filters out the current post from related articles", () => {
|
||||
const postsWithCurrent = [
|
||||
...mockRelatedPosts,
|
||||
{
|
||||
slug: "current-article",
|
||||
frontmatter: {
|
||||
title: "Current Article",
|
||||
description: "This is the current article",
|
||||
author: "Test Author",
|
||||
date: "2025-04-15",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
render(
|
||||
<RelatedArticles
|
||||
relatedPosts={postsWithCurrent}
|
||||
currentPostSlug="current-article"
|
||||
/>,
|
||||
);
|
||||
|
||||
// Should not render the current article
|
||||
expect(
|
||||
screen.queryByTestId("thumbnail-current-article"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// Should still render the other related articles
|
||||
expect(
|
||||
screen.getByTestId("thumbnail-related-article-1"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId("thumbnail-related-article-2"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId("thumbnail-related-article-3"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders nothing when no related posts", () => {
|
||||
const { container } = render(
|
||||
<RelatedArticles relatedPosts={[]} currentPostSlug="current-article" />,
|
||||
);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("renders nothing when all posts are filtered out", () => {
|
||||
const currentPostOnly = [
|
||||
{
|
||||
slug: "current-article",
|
||||
frontmatter: {
|
||||
title: "Current Article",
|
||||
description: "This is the current article",
|
||||
author: "Test Author",
|
||||
date: "2025-04-15",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const { container } = render(
|
||||
<RelatedArticles
|
||||
relatedPosts={currentPostOnly}
|
||||
currentPostSlug="current-article"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("has correct container styling", () => {
|
||||
render(
|
||||
<RelatedArticles
|
||||
relatedPosts={mockRelatedPosts}
|
||||
currentPostSlug="current-article"
|
||||
/>,
|
||||
);
|
||||
|
||||
const container = document.querySelector("section > div");
|
||||
expect(container).toHaveClass(
|
||||
"flex",
|
||||
"flex-col",
|
||||
"gap-[var(--spacing-scale-032)]",
|
||||
"lg:gap-[51px]",
|
||||
);
|
||||
});
|
||||
|
||||
it("has correct articles container styling", () => {
|
||||
render(
|
||||
<RelatedArticles
|
||||
relatedPosts={mockRelatedPosts}
|
||||
currentPostSlug="current-article"
|
||||
/>,
|
||||
);
|
||||
|
||||
const articlesContainer = document.querySelector("section > div > div");
|
||||
expect(articlesContainer).toHaveClass(
|
||||
"flex",
|
||||
"justify-center",
|
||||
"overflow-hidden",
|
||||
);
|
||||
});
|
||||
|
||||
it("applies correct responsive behavior for desktop", () => {
|
||||
// Set desktop width (must be > 1024px to be desktop, since lg breakpoint is 1024px)
|
||||
Object.defineProperty(window, "innerWidth", {
|
||||
writable: true,
|
||||
configurable: true,
|
||||
value: 1200,
|
||||
});
|
||||
|
||||
render(
|
||||
<RelatedArticles
|
||||
relatedPosts={mockRelatedPosts}
|
||||
currentPostSlug="current-article"
|
||||
/>,
|
||||
);
|
||||
|
||||
const carouselContainer = document.querySelector(
|
||||
"section > div > div > div",
|
||||
);
|
||||
expect(carouselContainer).toHaveClass(
|
||||
"overflow-x-auto",
|
||||
"scrollbar-hide",
|
||||
"cursor-grab",
|
||||
"active:cursor-grabbing",
|
||||
);
|
||||
});
|
||||
|
||||
it("applies correct responsive behavior for mobile", () => {
|
||||
// Set mobile width
|
||||
Object.defineProperty(window, "innerWidth", {
|
||||
writable: true,
|
||||
configurable: true,
|
||||
value: 768,
|
||||
});
|
||||
|
||||
render(
|
||||
<RelatedArticles
|
||||
relatedPosts={mockRelatedPosts}
|
||||
currentPostSlug="current-article"
|
||||
/>,
|
||||
);
|
||||
|
||||
const carouselContainer = document.querySelector(
|
||||
"section > div > div > div",
|
||||
);
|
||||
expect(carouselContainer).toHaveClass(
|
||||
"transition-transform",
|
||||
"duration-500",
|
||||
"ease-in-out",
|
||||
);
|
||||
});
|
||||
|
||||
it("handles single related article", () => {
|
||||
const singlePost = [mockRelatedPosts[0]];
|
||||
|
||||
render(
|
||||
<RelatedArticles
|
||||
relatedPosts={singlePost}
|
||||
currentPostSlug="current-article"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByTestId("thumbnail-related-article-1"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("thumbnail-related-article-2"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("thumbnail-related-article-3"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles two related articles", () => {
|
||||
const twoPosts = mockRelatedPosts.slice(0, 2);
|
||||
|
||||
render(
|
||||
<RelatedArticles
|
||||
relatedPosts={twoPosts}
|
||||
currentPostSlug="current-article"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByTestId("thumbnail-related-article-1"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId("thumbnail-related-article-2"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("thumbnail-related-article-3"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("has proper accessibility attributes", () => {
|
||||
render(
|
||||
<RelatedArticles
|
||||
relatedPosts={mockRelatedPosts}
|
||||
currentPostSlug="current-article"
|
||||
/>,
|
||||
);
|
||||
|
||||
const section = document.querySelector("section");
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies correct gap between articles", () => {
|
||||
render(
|
||||
<RelatedArticles
|
||||
relatedPosts={mockRelatedPosts}
|
||||
currentPostSlug="current-article"
|
||||
/>,
|
||||
);
|
||||
|
||||
const carouselContainer = document.querySelector(
|
||||
"section > div > div > div",
|
||||
);
|
||||
expect(carouselContainer).toHaveClass("gap-0");
|
||||
});
|
||||
|
||||
it("handles missing currentPostSlug gracefully", () => {
|
||||
render(<RelatedArticles relatedPosts={mockRelatedPosts} />);
|
||||
|
||||
// Should still render all articles
|
||||
expect(
|
||||
screen.getByTestId("thumbnail-related-article-1"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId("thumbnail-related-article-2"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId("thumbnail-related-article-3"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles malformed post data gracefully", () => {
|
||||
const malformedPosts = [
|
||||
{
|
||||
slug: "malformed-1",
|
||||
frontmatter: {
|
||||
title: "Malformed Post 1",
|
||||
description: "Test description",
|
||||
author: "Test Author",
|
||||
date: "2025-04-15",
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: "malformed-2",
|
||||
frontmatter: {
|
||||
title: "Malformed Post 2",
|
||||
description: "Test description",
|
||||
author: "Test Author",
|
||||
date: "2025-04-15",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
render(
|
||||
<RelatedArticles
|
||||
relatedPosts={malformedPosts}
|
||||
currentPostSlug="current-article"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("thumbnail-malformed-1")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("thumbnail-malformed-2")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,198 +0,0 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import SectionHeader from "../../app/components/SectionHeader";
|
||||
|
||||
describe("SectionHeader Component", () => {
|
||||
it("renders section header with title", () => {
|
||||
render(<SectionHeader title="Test Section" />);
|
||||
|
||||
expect(screen.getByRole("heading", { level: 2 })).toBeInTheDocument();
|
||||
// Check for both mobile and desktop versions of the title
|
||||
expect(screen.getAllByText("Test Section")).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("renders with subtitle when provided", () => {
|
||||
const subtitle = "This is a test subtitle";
|
||||
render(<SectionHeader title="Test Section" subtitle={subtitle} />);
|
||||
|
||||
expect(screen.getByText(subtitle)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders with titleLg when provided", () => {
|
||||
const titleLg = "Large Title for Desktop";
|
||||
render(<SectionHeader title="Test Section" titleLg={titleLg} />);
|
||||
|
||||
// Check for mobile title and desktop titleLg
|
||||
expect(screen.getByText("Test Section")).toBeInTheDocument();
|
||||
expect(screen.getByText(titleLg)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies variant classes correctly", () => {
|
||||
const { rerender } = render(
|
||||
<SectionHeader title="Default Header" variant="default" />,
|
||||
);
|
||||
let titleContainer = screen
|
||||
.getByRole("heading", { level: 2 })
|
||||
.closest("div");
|
||||
expect(titleContainer).toHaveClass(
|
||||
"lg:w-[369px]",
|
||||
"lg:h-[var(--spacing-scale-120)]",
|
||||
);
|
||||
|
||||
rerender(<SectionHeader title="Multi-line Header" variant="multi-line" />);
|
||||
titleContainer = screen.getByRole("heading", { level: 2 }).closest("div");
|
||||
expect(titleContainer).toHaveClass(
|
||||
"lg:w-[50%]",
|
||||
"lg:h-[var(--spacing-scale-120)]",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders responsive title spans", () => {
|
||||
render(<SectionHeader title="Test Section" />);
|
||||
|
||||
const mobileTitle = screen.getByText("Test Section", {
|
||||
selector: "span.block.lg\\:hidden",
|
||||
});
|
||||
const desktopTitle = screen.getByText("Test Section", {
|
||||
selector: "span.hidden.lg\\:block",
|
||||
});
|
||||
|
||||
expect(mobileTitle).toBeInTheDocument();
|
||||
expect(desktopTitle).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("uses titleLg for desktop when provided", () => {
|
||||
const titleLg = "Desktop Title";
|
||||
render(<SectionHeader title="Mobile Title" titleLg={titleLg} />);
|
||||
|
||||
const mobileTitle = screen.getByText("Mobile Title", {
|
||||
selector: "span.block.lg\\:hidden",
|
||||
});
|
||||
const desktopTitle = screen.getByText("Desktop Title", {
|
||||
selector: "span.hidden.lg\\:block",
|
||||
});
|
||||
|
||||
expect(mobileTitle).toBeInTheDocument();
|
||||
expect(desktopTitle).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("falls back to title for desktop when titleLg not provided", () => {
|
||||
render(<SectionHeader title="Test Section" />);
|
||||
|
||||
const mobileTitle = screen.getByText("Test Section", {
|
||||
selector: "span.block.lg\\:hidden",
|
||||
});
|
||||
const desktopTitle = screen.getByText("Test Section", {
|
||||
selector: "span.hidden.lg\\:block",
|
||||
});
|
||||
|
||||
expect(mobileTitle).toBeInTheDocument();
|
||||
expect(desktopTitle).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies proper responsive layout classes", () => {
|
||||
render(<SectionHeader title="Test Section" />);
|
||||
|
||||
const container = screen
|
||||
.getByRole("heading", { level: 2 })
|
||||
.closest("div").parentElement;
|
||||
expect(container).toHaveClass(
|
||||
"flex",
|
||||
"flex-col",
|
||||
"lg:flex-row",
|
||||
"lg:justify-between",
|
||||
);
|
||||
});
|
||||
|
||||
it("handles empty subtitle gracefully", () => {
|
||||
render(<SectionHeader title="Test Section" subtitle="" />);
|
||||
|
||||
expect(screen.getByRole("heading", { level: 2 })).toBeInTheDocument();
|
||||
// Empty subtitle should not cause issues - check that the paragraph element exists
|
||||
const subtitleContainer = screen
|
||||
.getByRole("heading", { level: 2 })
|
||||
.closest("div")
|
||||
.parentElement.querySelector("p");
|
||||
expect(subtitleContainer).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("maintains proper heading structure", () => {
|
||||
render(<SectionHeader title="Test Section" />);
|
||||
|
||||
const heading = screen.getByRole("heading", { level: 2 });
|
||||
expect(heading).toHaveTextContent("Test Section");
|
||||
expect(heading.tagName).toBe("H2");
|
||||
});
|
||||
|
||||
it("applies proper font classes", () => {
|
||||
render(<SectionHeader title="Test Section" />);
|
||||
|
||||
const heading = screen.getByRole("heading", { level: 2 });
|
||||
expect(heading).toHaveClass("font-bricolage-grotesque", "font-bold");
|
||||
});
|
||||
|
||||
it("applies proper text sizing", () => {
|
||||
render(<SectionHeader title="Test Section" />);
|
||||
|
||||
const heading = screen.getByRole("heading", { level: 2 });
|
||||
expect(heading).toHaveClass(
|
||||
"text-[28px]",
|
||||
"sm:text-[32px]",
|
||||
"lg:text-[32px]",
|
||||
"xl:text-[40px]",
|
||||
);
|
||||
});
|
||||
|
||||
it("applies proper line heights", () => {
|
||||
render(<SectionHeader title="Test Section" />);
|
||||
|
||||
const heading = screen.getByRole("heading", { level: 2 });
|
||||
expect(heading).toHaveClass(
|
||||
"leading-[36px]",
|
||||
"sm:leading-[40px]",
|
||||
"lg:leading-[40px]",
|
||||
"xl:leading-[52px]",
|
||||
);
|
||||
});
|
||||
|
||||
it("applies proper text colors", () => {
|
||||
render(<SectionHeader title="Test Section" />);
|
||||
|
||||
const heading = screen.getByRole("heading", { level: 2 });
|
||||
expect(heading).toHaveClass("text-[var(--color-content-default-primary)]");
|
||||
});
|
||||
|
||||
it("applies proper subtitle styling", () => {
|
||||
const subtitle = "Test Subtitle";
|
||||
render(<SectionHeader title="Test Section" subtitle={subtitle} />);
|
||||
|
||||
const subtitleElement = screen.getByText(subtitle);
|
||||
expect(subtitleElement).toHaveClass("font-inter", "font-normal");
|
||||
});
|
||||
|
||||
it("applies proper subtitle text sizing", () => {
|
||||
const subtitle = "Test Subtitle";
|
||||
render(<SectionHeader title="Test Section" subtitle={subtitle} />);
|
||||
|
||||
const subtitleElement = screen.getByText(subtitle);
|
||||
expect(subtitleElement).toHaveClass(
|
||||
"text-[18px]",
|
||||
"sm:text-[18px]",
|
||||
"lg:text-[24px]",
|
||||
"xl:text-[32px]",
|
||||
);
|
||||
});
|
||||
|
||||
it("applies proper subtitle colors", () => {
|
||||
const subtitle = "Test Subtitle";
|
||||
render(<SectionHeader title="Test Section" subtitle={subtitle} />);
|
||||
|
||||
const subtitleElement = screen.getByText(subtitle);
|
||||
expect(subtitleElement).toHaveClass(
|
||||
"text-[#484848]",
|
||||
"sm:text-[var(--color-content-default-tertiary)]",
|
||||
"lg:text-[var(--color-content-default-tertiary)]",
|
||||
"xl:text-[var(--color-content-default-tertiary)]",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,401 +0,0 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { expect, describe, it, vi } from "vitest";
|
||||
import { axe, toHaveNoViolations } from "jest-axe";
|
||||
import Select from "../../app/components/Select";
|
||||
|
||||
expect.extend(toHaveNoViolations);
|
||||
|
||||
describe("Select Component", () => {
|
||||
const defaultProps = {
|
||||
label: "Test Select",
|
||||
placeholder: "Select an option",
|
||||
options: [
|
||||
{ value: "option1", label: "Option 1" },
|
||||
{ value: "option2", label: "Option 2" },
|
||||
{ value: "option3", label: "Option 3" },
|
||||
],
|
||||
};
|
||||
|
||||
describe("Rendering", () => {
|
||||
it("renders with default props", () => {
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText("Test Select")).toBeInTheDocument();
|
||||
expect(screen.getByText("Select an option")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders without label when not provided", () => {
|
||||
render(
|
||||
<Select
|
||||
placeholder="Select an option"
|
||||
options={defaultProps.options}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText("Test Select")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Select an option")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders with horizontal label variant", () => {
|
||||
render(<Select {...defaultProps} labelVariant="horizontal" />);
|
||||
|
||||
const container = screen.getByText("Test Select").closest("div");
|
||||
expect(container).toHaveClass("flex", "items-center");
|
||||
});
|
||||
|
||||
it("renders with default label variant", () => {
|
||||
render(<Select {...defaultProps} labelVariant="default" />);
|
||||
|
||||
const container = screen.getByText("Test Select").closest("div");
|
||||
expect(container).toHaveClass("flex", "flex-col");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Size Variants", () => {
|
||||
it("renders small size correctly", () => {
|
||||
render(<Select {...defaultProps} size="small" />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
expect(selectButton).toHaveClass("h-[32px]");
|
||||
});
|
||||
|
||||
it("renders medium size correctly", () => {
|
||||
render(<Select {...defaultProps} size="medium" />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
expect(selectButton).toHaveClass("h-[36px]");
|
||||
});
|
||||
|
||||
it("renders large size correctly", () => {
|
||||
render(<Select {...defaultProps} size="large" />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
expect(selectButton).toHaveClass("h-[40px]");
|
||||
});
|
||||
|
||||
it("applies correct height for small horizontal label", () => {
|
||||
render(
|
||||
<Select {...defaultProps} size="small" labelVariant="horizontal" />,
|
||||
);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
expect(selectButton).toHaveClass("h-[30px]");
|
||||
});
|
||||
|
||||
it("applies correct height for small default label", () => {
|
||||
render(<Select {...defaultProps} size="small" labelVariant="default" />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
expect(selectButton).toHaveClass("h-[32px]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("State Variants", () => {
|
||||
it("renders default state", () => {
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
expect(selectButton).toHaveClass(
|
||||
"border-[var(--color-border-default-tertiary)]",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders hover state", () => {
|
||||
render(<Select {...defaultProps} state="hover" />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
expect(selectButton).toHaveClass(
|
||||
"shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders focus state", () => {
|
||||
render(<Select {...defaultProps} state="focus" />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
expect(selectButton).toHaveClass(
|
||||
"border-[var(--color-border-default-utility-info)]",
|
||||
);
|
||||
expect(selectButton).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
|
||||
});
|
||||
|
||||
it("renders error state", () => {
|
||||
render(<Select {...defaultProps} error={true} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
expect(selectButton).toHaveClass(
|
||||
"border-[var(--color-border-default-utility-negative)]",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders disabled state", () => {
|
||||
render(<Select {...defaultProps} disabled={true} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
expect(selectButton).toHaveClass("cursor-not-allowed");
|
||||
expect(selectButton).toHaveClass("opacity-40");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Interaction", () => {
|
||||
it("opens dropdown when clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
await user.click(selectButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Option 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Option 2")).toBeInTheDocument();
|
||||
expect(screen.getByText("Option 3")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("closes dropdown when clicked again", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
await user.click(selectButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Option 1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(selectButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("selects an option when clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = vi.fn();
|
||||
render(<Select {...defaultProps} onChange={onChange} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
await user.click(selectButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Option 1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByText("Option 1"));
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
target: {
|
||||
value: "option1",
|
||||
text: "Option 1",
|
||||
},
|
||||
});
|
||||
expect(screen.getByText("Option 1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("closes dropdown when option is selected", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
await user.click(selectButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Option 1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByText("Option 1"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("Option 2")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("does not open when disabled", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Select {...defaultProps} disabled={true} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
await user.click(selectButton);
|
||||
|
||||
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Keyboard Navigation", () => {
|
||||
it("opens dropdown with Enter key", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
selectButton.focus();
|
||||
await user.keyboard("{Enter}");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Option 1")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("opens dropdown with Space key", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
selectButton.focus();
|
||||
await user.keyboard(" ");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Option 1")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("closes dropdown with Escape key", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
await user.click(selectButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Option 1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.keyboard("{Escape}");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("does not respond to keyboard when disabled", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Select {...defaultProps} disabled={true} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
selectButton.focus();
|
||||
await user.keyboard("{Enter}");
|
||||
|
||||
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Click Outside", () => {
|
||||
it("closes dropdown when clicking outside", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<div>
|
||||
<Select {...defaultProps} />
|
||||
<div data-testid="outside">Outside element</div>
|
||||
</div>,
|
||||
);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
await user.click(selectButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Option 1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByTestId("outside"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Value Display", () => {
|
||||
it("shows placeholder when no value selected", () => {
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText("Select an option")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows selected value when option is selected", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
await user.click(selectButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Option 1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByText("Option 1"));
|
||||
|
||||
expect(screen.getByText("Option 1")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Select an option")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows selected value when value prop is provided", () => {
|
||||
render(<Select {...defaultProps} value="option2" />);
|
||||
|
||||
expect(screen.getByText("Option 2")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Accessibility", () => {
|
||||
it("has no accessibility violations", async () => {
|
||||
const { container } = render(<Select {...defaultProps} />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("has proper ARIA attributes", () => {
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
expect(selectButton).toHaveAttribute("aria-expanded", "false");
|
||||
expect(selectButton).toHaveAttribute("aria-haspopup", "listbox");
|
||||
});
|
||||
|
||||
it("updates aria-expanded when dropdown opens", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
await user.click(selectButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(selectButton).toHaveAttribute("aria-expanded", "true");
|
||||
});
|
||||
});
|
||||
|
||||
it("associates label with select button", () => {
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const label = screen.getByText("Test Select");
|
||||
const selectButton = screen.getByRole("button");
|
||||
|
||||
expect(label).toHaveAttribute("for", selectButton.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Focus Behavior", () => {
|
||||
it("enters focus state when tabbed to", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
await user.tab();
|
||||
|
||||
expect(selectButton).toHaveFocus();
|
||||
expect(selectButton).toHaveClass(
|
||||
"focus-visible:border-[var(--color-border-default-utility-info)]",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not enter focus state when clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Select {...defaultProps} />);
|
||||
|
||||
const selectButton = screen.getByRole("button");
|
||||
await user.click(selectButton);
|
||||
|
||||
expect(selectButton).toHaveFocus();
|
||||
// Focus state should not be applied on click, only on keyboard navigation
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,184 +0,0 @@
|
||||
import React from "react";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import Switch from "../../app/components/Switch";
|
||||
|
||||
describe("Switch Component", () => {
|
||||
it("renders with default props", () => {
|
||||
render(<Switch />);
|
||||
const switchButton = screen.getByRole("switch");
|
||||
expect(switchButton).toBeInTheDocument();
|
||||
expect(switchButton).toHaveAttribute("aria-checked", "false");
|
||||
});
|
||||
|
||||
it("renders with custom props", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(
|
||||
<Switch
|
||||
checked={true}
|
||||
onChange={handleChange}
|
||||
label="Test Switch"
|
||||
state="focus"
|
||||
/>,
|
||||
);
|
||||
|
||||
const switchButton = screen.getByRole("switch");
|
||||
expect(switchButton).toHaveAttribute("aria-checked", "true");
|
||||
expect(screen.getByText("Test Switch")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles checked prop correctly", () => {
|
||||
const { rerender } = render(<Switch checked={false} />);
|
||||
let switchButton = screen.getByRole("switch");
|
||||
expect(switchButton).toHaveAttribute("aria-checked", "false");
|
||||
|
||||
rerender(<Switch checked={true} />);
|
||||
switchButton = screen.getByRole("switch");
|
||||
expect(switchButton).toHaveAttribute("aria-checked", "true");
|
||||
});
|
||||
|
||||
it("handles state prop correctly", () => {
|
||||
const { rerender } = render(<Switch state="default" />);
|
||||
let switchButton = screen.getByRole("switch");
|
||||
expect(switchButton).not.toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
|
||||
|
||||
rerender(<Switch state="focus" />);
|
||||
switchButton = screen.getByRole("switch");
|
||||
expect(switchButton).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
|
||||
});
|
||||
|
||||
it("calls onChange when clicked", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<Switch onChange={handleChange} />);
|
||||
|
||||
const switchButton = screen.getByRole("switch");
|
||||
fireEvent.click(switchButton);
|
||||
expect(handleChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("calls onFocus when focused", () => {
|
||||
const handleFocus = vi.fn();
|
||||
render(<Switch onFocus={handleFocus} />);
|
||||
|
||||
const switchButton = screen.getByRole("switch");
|
||||
fireEvent.focus(switchButton);
|
||||
expect(handleFocus).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("calls onBlur when blurred", () => {
|
||||
const handleBlur = vi.fn();
|
||||
render(<Switch onBlur={handleBlur} />);
|
||||
|
||||
const switchButton = screen.getByRole("switch");
|
||||
fireEvent.blur(switchButton);
|
||||
expect(handleBlur).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("handles keyboard events correctly", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<Switch onChange={handleChange} />);
|
||||
|
||||
const switchButton = screen.getByRole("switch");
|
||||
|
||||
// Test Enter key
|
||||
fireEvent.keyDown(switchButton, { key: "Enter" });
|
||||
expect(handleChange).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Test Space key
|
||||
fireEvent.keyDown(switchButton, { key: " " });
|
||||
expect(handleChange).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Test other key (should not trigger)
|
||||
fireEvent.keyDown(switchButton, { key: "Tab" });
|
||||
expect(handleChange).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("applies correct classes for different states", () => {
|
||||
const { rerender } = render(<Switch checked={false} />);
|
||||
let switchButton = screen.getByRole("switch");
|
||||
expect(switchButton).toHaveClass("cursor-pointer");
|
||||
|
||||
rerender(<Switch checked={true} />);
|
||||
switchButton = screen.getByRole("switch");
|
||||
expect(switchButton).toHaveClass("cursor-pointer");
|
||||
});
|
||||
|
||||
it("applies correct track styles based on checked state", () => {
|
||||
const { rerender } = render(<Switch checked={false} />);
|
||||
let switchButton = screen.getByRole("switch");
|
||||
let track = switchButton.querySelector("div");
|
||||
expect(track).toHaveClass("bg-[var(--color-surface-default-tertiary)]");
|
||||
|
||||
rerender(<Switch checked={true} />);
|
||||
switchButton = screen.getByRole("switch");
|
||||
track = switchButton.querySelector("div");
|
||||
expect(track).toHaveClass("bg-[var(--color-surface-inverse-tertiary)]");
|
||||
|
||||
switchButton = screen.getByRole("switch");
|
||||
track = switchButton.querySelector("div");
|
||||
expect(track).toHaveClass("bg-[var(--color-surface-inverse-tertiary)]");
|
||||
});
|
||||
|
||||
it("applies correct focus styles", () => {
|
||||
const { rerender } = render(<Switch state="default" />);
|
||||
let switchButton = screen.getByRole("switch");
|
||||
expect(switchButton).not.toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
|
||||
|
||||
rerender(<Switch state="focus" />);
|
||||
switchButton = screen.getByRole("switch");
|
||||
expect(switchButton).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
|
||||
});
|
||||
|
||||
it("applies correct base classes", () => {
|
||||
render(<Switch />);
|
||||
const switchButton = screen.getByRole("switch");
|
||||
expect(switchButton).toHaveClass(
|
||||
"relative",
|
||||
"inline-flex",
|
||||
"items-center",
|
||||
"cursor-pointer",
|
||||
"transition-all",
|
||||
"duration-200",
|
||||
"focus:outline-none",
|
||||
"focus-visible:shadow-[0_0_5px_3px_#3281F8]",
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards ref correctly", () => {
|
||||
const ref = React.createRef();
|
||||
render(<Switch ref={ref} />);
|
||||
expect(ref.current).toBeInstanceOf(HTMLButtonElement);
|
||||
});
|
||||
|
||||
it("applies custom className", () => {
|
||||
render(<Switch className="custom-class" />);
|
||||
const switchButton = screen.getByRole("switch");
|
||||
expect(switchButton).toHaveClass("custom-class");
|
||||
});
|
||||
|
||||
it("renders label when provided", () => {
|
||||
render(<Switch label="Test Label" />);
|
||||
expect(screen.getByText("Test Label")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render label when not provided", () => {
|
||||
render(<Switch />);
|
||||
expect(screen.queryByText("Switch label")).not.toBeInTheDocument();
|
||||
// Should have aria-label for accessibility
|
||||
const switchButton = screen.getByRole("switch");
|
||||
expect(switchButton).toHaveAttribute("aria-label", "Toggle switch");
|
||||
});
|
||||
|
||||
it("applies correct label styles", () => {
|
||||
render(<Switch label="Test Label" />);
|
||||
const label = screen.getByText("Test Label");
|
||||
expect(label).toHaveClass(
|
||||
"ml-[var(--measures-spacing-008)]",
|
||||
"font-inter",
|
||||
"font-normal",
|
||||
"text-[14px]",
|
||||
"leading-[20px]",
|
||||
"text-[var(--color-content-default-primary)]",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,203 +0,0 @@
|
||||
import { expect, test, describe, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import TextArea from "../../app/components/TextArea";
|
||||
|
||||
describe("TextArea", () => {
|
||||
test("renders with default props", () => {
|
||||
render(<TextArea />);
|
||||
const textarea = screen.getByRole("textbox");
|
||||
expect(textarea).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with label", () => {
|
||||
render(<TextArea label="Test Label" />);
|
||||
expect(screen.getByText("Test Label")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Test Label")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with placeholder", () => {
|
||||
render(<TextArea placeholder="Enter text..." />);
|
||||
expect(screen.getByPlaceholderText("Enter text...")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with value", () => {
|
||||
render(<TextArea value="Test value" />);
|
||||
const textarea = screen.getByRole("textbox");
|
||||
expect(textarea).toHaveValue("Test value");
|
||||
});
|
||||
|
||||
test("renders with different sizes", () => {
|
||||
const { rerender } = render(<TextArea size="small" label="Small" />);
|
||||
let textarea = screen.getByRole("textbox");
|
||||
expect(textarea).toHaveClass("h-[60px]");
|
||||
|
||||
rerender(<TextArea size="medium" label="Medium" />);
|
||||
textarea = screen.getByRole("textbox");
|
||||
expect(textarea).toHaveClass("h-[100px]");
|
||||
|
||||
rerender(<TextArea size="large" label="Large" />);
|
||||
textarea = screen.getByRole("textbox");
|
||||
expect(textarea).toHaveClass("h-[150px]");
|
||||
});
|
||||
|
||||
test("renders with horizontal label variant", () => {
|
||||
render(<TextArea labelVariant="horizontal" label="Horizontal Label" />);
|
||||
const container = screen.getByRole("textbox").closest("div").parentElement;
|
||||
expect(container).toHaveClass("flex", "items-center", "gap-[12px]");
|
||||
});
|
||||
|
||||
test("renders with default label variant", () => {
|
||||
render(<TextArea labelVariant="default" label="Default Label" />);
|
||||
const container = screen.getByRole("textbox").closest("div").parentElement;
|
||||
expect(container).toHaveClass("flex", "flex-col");
|
||||
});
|
||||
|
||||
test("applies disabled state", () => {
|
||||
render(<TextArea disabled />);
|
||||
const textarea = screen.getByRole("textbox");
|
||||
expect(textarea).toBeDisabled();
|
||||
});
|
||||
|
||||
test("applies error state", () => {
|
||||
render(<TextArea error />);
|
||||
const textarea = screen.getByRole("textbox");
|
||||
expect(textarea).toHaveClass(
|
||||
"border-[var(--color-border-default-utility-negative)]",
|
||||
);
|
||||
});
|
||||
|
||||
test("applies different states", () => {
|
||||
const { rerender } = render(<TextArea state="active" />);
|
||||
let textarea = screen.getByRole("textbox");
|
||||
expect(textarea).toHaveClass(
|
||||
"border-[var(--color-border-default-tertiary)]",
|
||||
);
|
||||
|
||||
rerender(<TextArea state="hover" />);
|
||||
textarea = screen.getByRole("textbox");
|
||||
expect(textarea).toHaveClass(
|
||||
"shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
|
||||
);
|
||||
|
||||
rerender(<TextArea state="focus" />);
|
||||
textarea = screen.getByRole("textbox");
|
||||
expect(textarea).toHaveClass(
|
||||
"border-[var(--color-border-default-utility-info)]",
|
||||
"shadow-[0_0_5px_3px_#3281F8]",
|
||||
);
|
||||
});
|
||||
|
||||
test("calls onChange when text changes", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleChange = vi.fn();
|
||||
render(<TextArea onChange={handleChange} />);
|
||||
|
||||
const textarea = screen.getByRole("textbox");
|
||||
await user.type(textarea, "test");
|
||||
|
||||
expect(handleChange).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
test("calls onFocus when focused", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleFocus = vi.fn();
|
||||
render(<TextArea onFocus={handleFocus} />);
|
||||
|
||||
const textarea = screen.getByRole("textbox");
|
||||
await user.click(textarea);
|
||||
|
||||
expect(handleFocus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("calls onBlur when blurred", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleBlur = vi.fn();
|
||||
render(<TextArea onBlur={handleBlur} />);
|
||||
|
||||
const textarea = screen.getByRole("textbox");
|
||||
await user.click(textarea);
|
||||
await user.tab();
|
||||
|
||||
expect(handleBlur).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("does not call onChange when disabled", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleChange = vi.fn();
|
||||
render(<TextArea disabled onChange={handleChange} />);
|
||||
|
||||
const textarea = screen.getByRole("textbox");
|
||||
await user.type(textarea, "test");
|
||||
|
||||
expect(handleChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("applies custom className", () => {
|
||||
render(<TextArea className="custom-class" />);
|
||||
const textarea = screen.getByRole("textbox");
|
||||
expect(textarea).toHaveClass("custom-class");
|
||||
});
|
||||
|
||||
test("forwards ref", () => {
|
||||
const ref = vi.fn();
|
||||
render(<TextArea ref={ref} />);
|
||||
expect(ref).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("applies correct height for small horizontal label", () => {
|
||||
render(
|
||||
<TextArea
|
||||
size="small"
|
||||
labelVariant="horizontal"
|
||||
label="Small Horizontal"
|
||||
/>,
|
||||
);
|
||||
const textarea = screen.getByRole("textbox");
|
||||
expect(textarea).toHaveClass("h-[60px]");
|
||||
});
|
||||
|
||||
test("applies correct height for medium horizontal label", () => {
|
||||
render(
|
||||
<TextArea
|
||||
size="medium"
|
||||
labelVariant="horizontal"
|
||||
label="Medium Horizontal"
|
||||
/>,
|
||||
);
|
||||
const textarea = screen.getByRole("textbox");
|
||||
expect(textarea).toHaveClass("h-[110px]");
|
||||
});
|
||||
|
||||
test("applies correct border radius for different sizes", () => {
|
||||
const { rerender } = render(<TextArea size="small" />);
|
||||
let textarea = screen.getByRole("textbox");
|
||||
expect(textarea).toHaveStyle({
|
||||
borderRadius: "var(--measures-radius-xsmall)",
|
||||
});
|
||||
|
||||
rerender(<TextArea size="medium" />);
|
||||
textarea = screen.getByRole("textbox");
|
||||
expect(textarea).toHaveStyle({
|
||||
borderRadius: "var(--measures-radius-xsmall)",
|
||||
});
|
||||
|
||||
rerender(<TextArea size="large" />);
|
||||
textarea = screen.getByRole("textbox");
|
||||
expect(textarea).toHaveStyle({
|
||||
borderRadius: "var(--measures-radius-small)",
|
||||
});
|
||||
});
|
||||
|
||||
test("applies correct text color", () => {
|
||||
render(<TextArea />);
|
||||
const textarea = screen.getByRole("textbox");
|
||||
expect(textarea).toHaveClass("text-[var(--color-content-default-primary)]");
|
||||
});
|
||||
|
||||
test("applies correct label color", () => {
|
||||
render(<TextArea label="Test Label" />);
|
||||
const label = screen.getByText("Test Label");
|
||||
expect(label).toHaveClass("text-[var(--color-content-default-secondary)]");
|
||||
});
|
||||
});
|
||||
@@ -1,195 +0,0 @@
|
||||
import { expect, test, describe, vi } from "vitest";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import Toggle from "../../app/components/Toggle";
|
||||
|
||||
describe("Toggle Component", () => {
|
||||
test("renders with default props", () => {
|
||||
render(<Toggle label="Test Toggle" />);
|
||||
|
||||
const toggle = screen.getByRole("switch");
|
||||
const label = screen.getByText("Test Toggle");
|
||||
|
||||
expect(toggle).toBeInTheDocument();
|
||||
expect(label).toBeInTheDocument();
|
||||
expect(toggle).toHaveAttribute("type", "button");
|
||||
});
|
||||
|
||||
test("renders with custom props", () => {
|
||||
render(
|
||||
<Toggle
|
||||
label="Custom Toggle"
|
||||
checked={true}
|
||||
disabled={true}
|
||||
className="custom-class"
|
||||
/>,
|
||||
);
|
||||
|
||||
const toggle = screen.getByRole("switch");
|
||||
expect(toggle).toBeInTheDocument();
|
||||
expect(toggle).toHaveAttribute("aria-checked", "true");
|
||||
expect(toggle).toHaveAttribute("disabled");
|
||||
});
|
||||
|
||||
test("handles checked state", () => {
|
||||
const { rerender } = render(<Toggle label="Test Toggle" checked={false} />);
|
||||
|
||||
let toggle = screen.getByRole("switch");
|
||||
expect(toggle).toHaveAttribute("aria-checked", "false");
|
||||
|
||||
rerender(<Toggle label="Test Toggle" checked={true} />);
|
||||
toggle = screen.getByRole("switch");
|
||||
expect(toggle).toHaveAttribute("aria-checked", "true");
|
||||
});
|
||||
|
||||
test("handles disabled state", () => {
|
||||
render(<Toggle label="Test Toggle" disabled={true} />);
|
||||
|
||||
const toggle = screen.getByRole("switch");
|
||||
expect(toggle).toHaveAttribute("disabled");
|
||||
expect(toggle).toHaveClass("cursor-not-allowed");
|
||||
});
|
||||
|
||||
test("handles state prop", () => {
|
||||
const { rerender } = render(<Toggle label="Test Toggle" state="focus" />);
|
||||
|
||||
let toggle = screen.getByRole("switch");
|
||||
expect(toggle).toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
|
||||
|
||||
rerender(<Toggle label="Test Toggle" state="default" />);
|
||||
toggle = screen.getByRole("switch");
|
||||
expect(toggle).not.toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
|
||||
});
|
||||
|
||||
test("handles showIcon and icon props", () => {
|
||||
render(<Toggle label="Test Toggle" showIcon={true} icon="I" />);
|
||||
|
||||
const toggle = screen.getByRole("switch");
|
||||
expect(toggle).toHaveTextContent("I");
|
||||
});
|
||||
|
||||
test("handles showText and text props", () => {
|
||||
render(<Toggle label="Test Toggle" showText={true} text="Toggle" />);
|
||||
|
||||
const toggle = screen.getByRole("switch");
|
||||
expect(toggle).toHaveTextContent("Toggle");
|
||||
});
|
||||
|
||||
test("handles both icon and text", () => {
|
||||
render(
|
||||
<Toggle
|
||||
label="Test Toggle"
|
||||
showIcon={true}
|
||||
showText={true}
|
||||
icon="I"
|
||||
text="Toggle"
|
||||
/>,
|
||||
);
|
||||
|
||||
const toggle = screen.getByRole("switch");
|
||||
expect(toggle).toHaveTextContent("I");
|
||||
expect(toggle).toHaveTextContent("Toggle");
|
||||
});
|
||||
|
||||
test("calls onChange when clicked", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<Toggle label="Test Toggle" onChange={handleChange} />);
|
||||
|
||||
const toggle = screen.getByRole("switch");
|
||||
fireEvent.click(toggle);
|
||||
|
||||
expect(handleChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("does not call onChange when disabled", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(
|
||||
<Toggle label="Test Toggle" disabled={true} onChange={handleChange} />,
|
||||
);
|
||||
|
||||
const toggle = screen.getByRole("switch");
|
||||
fireEvent.click(toggle);
|
||||
|
||||
expect(handleChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("applies correct classes for different states", () => {
|
||||
const { rerender } = render(<Toggle label="Test Toggle" checked={false} />);
|
||||
|
||||
let toggle = screen.getByRole("switch");
|
||||
expect(toggle).toHaveClass("bg-[var(--color-surface-default-primary)]");
|
||||
|
||||
rerender(<Toggle label="Test Toggle" checked={true} />);
|
||||
toggle = screen.getByRole("switch");
|
||||
expect(toggle).toHaveClass("bg-[var(--color-magenta-magenta100)]");
|
||||
|
||||
rerender(<Toggle label="Test Toggle" disabled={true} />);
|
||||
toggle = screen.getByRole("switch");
|
||||
expect(toggle).toHaveClass("bg-[var(--color-surface-default-tertiary)]");
|
||||
});
|
||||
|
||||
test("applies hover classes when not checked", () => {
|
||||
render(<Toggle label="Test Toggle" checked={false} />);
|
||||
|
||||
const toggle = screen.getByRole("switch");
|
||||
expect(toggle).toHaveClass(
|
||||
"hover:!bg-[var(--color-surface-default-secondary)]",
|
||||
);
|
||||
});
|
||||
|
||||
test("does not apply hover classes when checked", () => {
|
||||
render(<Toggle label="Test Toggle" checked={true} />);
|
||||
|
||||
const toggle = screen.getByRole("switch");
|
||||
expect(toggle).not.toHaveClass(
|
||||
"hover:!bg-[var(--color-surface-default-secondary)]",
|
||||
);
|
||||
});
|
||||
|
||||
test("applies focus-visible classes", () => {
|
||||
render(<Toggle label="Test Toggle" />);
|
||||
|
||||
const toggle = screen.getByRole("switch");
|
||||
expect(toggle).toHaveClass("focus-visible:shadow-[0_0_5px_1px_#3281F8]");
|
||||
});
|
||||
|
||||
test("applies correct size classes", () => {
|
||||
render(<Toggle label="Test Toggle" />);
|
||||
|
||||
const toggle = screen.getByRole("switch");
|
||||
expect(toggle).toHaveClass("h-[var(--measures-sizing-032)]");
|
||||
expect(toggle).toHaveClass("px-[16px]");
|
||||
expect(toggle).toHaveClass("py-[8px]");
|
||||
expect(toggle).toHaveClass("gap-[4px]");
|
||||
});
|
||||
|
||||
test("applies correct text classes", () => {
|
||||
render(<Toggle label="Test Toggle" />);
|
||||
|
||||
const toggle = screen.getByRole("switch");
|
||||
expect(toggle).toHaveClass("text-[12px]");
|
||||
expect(toggle).toHaveClass("leading-[16px]");
|
||||
});
|
||||
|
||||
test("applies correct label classes", () => {
|
||||
render(<Toggle label="Test Toggle" />);
|
||||
|
||||
const label = screen.getByText("Test Toggle");
|
||||
expect(label).toHaveClass("text-[12px]");
|
||||
expect(label).toHaveClass("leading-[16px]");
|
||||
expect(label).toHaveClass("text-[var(--color-content-default-secondary)]");
|
||||
});
|
||||
|
||||
test("forwards ref correctly", () => {
|
||||
const ref = vi.fn();
|
||||
render(<Toggle label="Test Toggle" ref={ref} />);
|
||||
|
||||
expect(ref).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("applies custom className", () => {
|
||||
render(<Toggle label="Test Toggle" className="custom-class" />);
|
||||
|
||||
const toggle = screen.getByRole("switch");
|
||||
expect(toggle).toHaveClass("custom-class");
|
||||
});
|
||||
});
|
||||
@@ -1,213 +0,0 @@
|
||||
import React from "react";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import ToggleGroup from "../../app/components/ToggleGroup";
|
||||
|
||||
describe("ToggleGroup Component", () => {
|
||||
it("renders with default props", () => {
|
||||
render(<ToggleGroup>Test Content</ToggleGroup>);
|
||||
const toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toBeInTheDocument();
|
||||
expect(toggleGroup).toHaveTextContent("Test Content");
|
||||
});
|
||||
|
||||
it("renders with custom props", () => {
|
||||
render(
|
||||
<ToggleGroup position="middle" state="selected" showText={true}>
|
||||
Custom Content
|
||||
</ToggleGroup>,
|
||||
);
|
||||
const toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toBeInTheDocument();
|
||||
expect(toggleGroup).toHaveTextContent("Custom Content");
|
||||
});
|
||||
|
||||
it("handles position prop correctly", () => {
|
||||
const { rerender } = render(
|
||||
<ToggleGroup position="left">Left</ToggleGroup>,
|
||||
);
|
||||
let toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass(
|
||||
"rounded-l-[var(--measures-radius-medium)]",
|
||||
"rounded-r-none",
|
||||
);
|
||||
|
||||
rerender(<ToggleGroup position="middle">Middle</ToggleGroup>);
|
||||
toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass("rounded-none");
|
||||
|
||||
rerender(<ToggleGroup position="right">Right</ToggleGroup>);
|
||||
toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass(
|
||||
"rounded-r-[var(--measures-radius-medium)]",
|
||||
"rounded-l-none",
|
||||
);
|
||||
});
|
||||
|
||||
it("handles state prop correctly", () => {
|
||||
const { rerender } = render(
|
||||
<ToggleGroup state="default">Default</ToggleGroup>,
|
||||
);
|
||||
let toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass(
|
||||
"bg-[var(--color-surface-default-primary)]",
|
||||
);
|
||||
|
||||
rerender(<ToggleGroup state="hover">Hover</ToggleGroup>);
|
||||
toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass("bg-[var(--color-magenta-magenta100)]");
|
||||
|
||||
rerender(<ToggleGroup state="focus">Focus</ToggleGroup>);
|
||||
toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass(
|
||||
"bg-[var(--color-surface-default-primary)]",
|
||||
"shadow-[0_0_5px_1px_#3281F8]",
|
||||
);
|
||||
|
||||
rerender(<ToggleGroup state="selected">Selected</ToggleGroup>);
|
||||
toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass(
|
||||
"bg-[var(--color-magenta-magenta100)]",
|
||||
"shadow-[inset_0_0_0_1px_var(--color-border-default-secondary)]",
|
||||
);
|
||||
});
|
||||
|
||||
it("handles showText prop correctly", () => {
|
||||
const { rerender } = render(
|
||||
<ToggleGroup showText={true}>Visible Text</ToggleGroup>,
|
||||
);
|
||||
let toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveTextContent("Visible Text");
|
||||
|
||||
rerender(<ToggleGroup showText={false}>☰</ToggleGroup>);
|
||||
toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveTextContent("☰");
|
||||
});
|
||||
|
||||
it("calls onChange when clicked", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<ToggleGroup onChange={handleChange}>Clickable</ToggleGroup>);
|
||||
const toggleGroup = screen.getByRole("button");
|
||||
|
||||
fireEvent.click(toggleGroup);
|
||||
expect(handleChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("calls onFocus when focused", () => {
|
||||
const handleFocus = vi.fn();
|
||||
render(<ToggleGroup onFocus={handleFocus}>Focusable</ToggleGroup>);
|
||||
const toggleGroup = screen.getByRole("button");
|
||||
|
||||
fireEvent.focus(toggleGroup);
|
||||
expect(handleFocus).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("calls onBlur when blurred", () => {
|
||||
const handleBlur = vi.fn();
|
||||
render(<ToggleGroup onBlur={handleBlur}>Blurable</ToggleGroup>);
|
||||
const toggleGroup = screen.getByRole("button");
|
||||
|
||||
fireEvent.blur(toggleGroup);
|
||||
expect(handleBlur).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("handles keyboard events correctly", () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<ToggleGroup onChange={handleChange}>Keyboard</ToggleGroup>);
|
||||
const toggleGroup = screen.getByRole("button");
|
||||
|
||||
// Test Enter key
|
||||
fireEvent.keyDown(toggleGroup, { key: "Enter" });
|
||||
expect(handleChange).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Test Space key
|
||||
fireEvent.keyDown(toggleGroup, { key: " " });
|
||||
expect(handleChange).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Test other key (should not trigger)
|
||||
fireEvent.keyDown(toggleGroup, { key: "Escape" });
|
||||
expect(handleChange).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("applies correct classes for different states", () => {
|
||||
const { rerender } = render(
|
||||
<ToggleGroup state="default">Default</ToggleGroup>,
|
||||
);
|
||||
let toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass(
|
||||
"bg-[var(--color-surface-default-primary)]",
|
||||
);
|
||||
|
||||
rerender(<ToggleGroup state="hover">Hover</ToggleGroup>);
|
||||
toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass("bg-[var(--color-magenta-magenta100)]");
|
||||
|
||||
rerender(<ToggleGroup state="focus">Focus</ToggleGroup>);
|
||||
toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
|
||||
|
||||
rerender(<ToggleGroup state="selected">Selected</ToggleGroup>);
|
||||
toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass(
|
||||
"bg-[var(--color-magenta-magenta100)]",
|
||||
"shadow-[inset_0_0_0_1px_var(--color-border-default-secondary)]",
|
||||
);
|
||||
});
|
||||
|
||||
it("applies correct position classes", () => {
|
||||
const { rerender } = render(
|
||||
<ToggleGroup position="left">Left</ToggleGroup>,
|
||||
);
|
||||
let toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass(
|
||||
"rounded-l-[var(--measures-radius-medium)]",
|
||||
"rounded-r-none",
|
||||
);
|
||||
|
||||
rerender(<ToggleGroup position="middle">Middle</ToggleGroup>);
|
||||
toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass("rounded-none");
|
||||
|
||||
rerender(<ToggleGroup position="right">Right</ToggleGroup>);
|
||||
toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass(
|
||||
"rounded-r-[var(--measures-radius-medium)]",
|
||||
"rounded-l-none",
|
||||
);
|
||||
});
|
||||
|
||||
it("applies correct base classes", () => {
|
||||
render(<ToggleGroup>Base</ToggleGroup>);
|
||||
const toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass(
|
||||
"py-[var(--measures-spacing-008)]",
|
||||
"px-[var(--measures-spacing-008)]",
|
||||
"gap-[var(--measures-spacing-008)]",
|
||||
"font-inter",
|
||||
"font-medium",
|
||||
"text-[12px]",
|
||||
"leading-[12px]",
|
||||
"cursor-pointer",
|
||||
"transition-all",
|
||||
"duration-200",
|
||||
"focus:outline-none",
|
||||
"focus-visible:shadow-[0_0_5px_1px_#3281F8]",
|
||||
"hover:bg-[var(--color-magenta-magenta100)]",
|
||||
"flex",
|
||||
"items-center",
|
||||
"justify-center",
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards ref correctly", () => {
|
||||
const ref = React.createRef();
|
||||
render(<ToggleGroup ref={ref}>Ref Test</ToggleGroup>);
|
||||
expect(ref.current).toBeInstanceOf(HTMLButtonElement);
|
||||
});
|
||||
|
||||
it("applies custom className", () => {
|
||||
render(<ToggleGroup className="custom-class">Custom</ToggleGroup>);
|
||||
const toggleGroup = screen.getByRole("button");
|
||||
expect(toggleGroup).toHaveClass("custom-class");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user